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": { "by_strategy": {
"momentum": { "momentum": {
"count": 35, "count": 92,
"wins_1d": 29, "wins_1d": 45,
"losses_1d": 6, "losses_1d": 33,
"wins_7d": 5, "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, "losses_7d": 0,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": 19.7,
"avg_return_7d": 0, "avg_return_7d": 19.7,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 82.9, "win_rate_1d": 100.0,
"win_rate_7d": 100.0 "win_rate_7d": 100.0
}, },
"insider_buying": { "insider_buying": {
"count": 8, "count": 21,
"wins_1d": 6, "wins_1d": 15,
"losses_1d": 2, "losses_1d": 6,
"wins_7d": 2, "wins_7d": 10,
"losses_7d": 0, "losses_7d": 5,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": 0.84,
"avg_return_7d": 0, "avg_return_7d": 0.18,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 75.0, "win_rate_1d": 71.4,
"win_rate_7d": 100.0 "win_rate_7d": 66.7
}, },
"options_flow": { "options_flow": {
"count": 5, "count": 5,
@ -37,53 +51,26 @@
"losses_7d": 0, "losses_7d": 0,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": 3.09,
"avg_return_7d": 0, "avg_return_7d": 0,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 80.0 "win_rate_1d": 80.0
}, },
"earnings_calendar": { "earnings_calendar": {
"count": 4, "count": 17,
"wins_1d": 1, "wins_1d": 6,
"losses_1d": 3, "losses_1d": 11,
"wins_7d": 0, "wins_7d": 4,
"losses_7d": 0, "losses_7d": 8,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": -0.23,
"avg_return_7d": 0, "avg_return_7d": 0.36,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 25.0 "win_rate_1d": 35.3,
"win_rate_7d": 33.3
}, },
"Momentum": { "contrarian_value": {
"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": {
"count": 6, "count": 6,
"wins_1d": 3, "wins_1d": 3,
"losses_1d": 3, "losses_1d": 3,
@ -91,67 +78,39 @@
"losses_7d": 3, "losses_7d": 3,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": -4.91,
"avg_return_7d": 0, "avg_return_7d": -4.91,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 50.0, "win_rate_1d": 50.0,
"win_rate_7d": 50.0 "win_rate_7d": 50.0
}, },
"Earnings Play": { "news_catalyst": {
"count": 3, "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, "wins_1d": 0,
"losses_1d": 1, "losses_1d": 3,
"wins_7d": 0, "wins_7d": 0,
"losses_7d": 1, "losses_7d": 3,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": -13.55,
"avg_return_7d": 0, "avg_return_7d": -13.55,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 0.0, "win_rate_1d": 0.0,
"win_rate_7d": 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": { "short_squeeze": {
"count": 7, "count": 10,
"wins_1d": 4, "wins_1d": 5,
"losses_1d": 3, "losses_1d": 5,
"wins_7d": 2, "wins_7d": 4,
"losses_7d": 2, "losses_7d": 3,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": 0.56,
"avg_return_7d": 0, "avg_return_7d": 0.85,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 57.1, "win_rate_1d": 50.0,
"win_rate_7d": 50.0 "win_rate_7d": 57.1
}, },
"early_accumulation": { "early_accumulation": {
"count": 1, "count": 1,
@ -161,8 +120,8 @@
"losses_7d": 0, "losses_7d": 0,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": 20.41,
"avg_return_7d": 0, "avg_return_7d": 20.41,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 100.0, "win_rate_1d": 100.0,
"win_rate_7d": 100.0 "win_rate_7d": 100.0
@ -175,26 +134,12 @@
"losses_7d": 4, "losses_7d": 4,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": -2.0,
"avg_return_7d": 0, "avg_return_7d": -2.06,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 28.6, "win_rate_1d": 28.6,
"win_rate_7d": 33.3 "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": { "analyst_upgrade": {
"count": 8, "count": 8,
"wins_1d": 6, "wins_1d": 6,
@ -203,8 +148,8 @@
"losses_7d": 2, "losses_7d": 2,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": 1.32,
"avg_return_7d": 0, "avg_return_7d": -0.1,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 75.0, "win_rate_1d": 75.0,
"win_rate_7d": 66.7 "win_rate_7d": 66.7
@ -217,8 +162,8 @@
"losses_7d": 1, "losses_7d": 1,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": -19.2,
"avg_return_7d": 0, "avg_return_7d": -19.2,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 0.0, "win_rate_1d": 0.0,
"win_rate_7d": 0.0 "win_rate_7d": 0.0
@ -231,26 +176,12 @@
"losses_7d": 2, "losses_7d": 2,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": -8.9,
"avg_return_7d": 0, "avg_return_7d": -8.9,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 0.0, "win_rate_1d": 0.0,
"win_rate_7d": 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": { "undiscovered_dd": {
"count": 2, "count": 2,
"wins_1d": 2, "wins_1d": 2,
@ -259,36 +190,8 @@
"losses_7d": 0, "losses_7d": 0,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": 6.44,
"avg_return_7d": 0, "avg_return_7d": 6.44,
"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_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 100.0, "win_rate_1d": 100.0,
"win_rate_7d": 100.0 "win_rate_7d": 100.0
@ -301,7 +204,7 @@
"losses_7d": 0, "losses_7d": 0,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": -3.38,
"avg_return_7d": 0, "avg_return_7d": 0,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 50.0 "win_rate_1d": 50.0
@ -314,7 +217,7 @@
"losses_7d": 0, "losses_7d": 0,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": 0.93,
"avg_return_7d": 0, "avg_return_7d": 0,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 50.0 "win_rate_1d": 50.0
@ -327,7 +230,7 @@
"losses_7d": 0, "losses_7d": 0,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": -5.11,
"avg_return_7d": 0, "avg_return_7d": 0,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 0.0 "win_rate_1d": 0.0
@ -340,7 +243,7 @@
"losses_7d": 0, "losses_7d": 0,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": -1.47,
"avg_return_7d": 0, "avg_return_7d": 0,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 50.0 "win_rate_1d": 50.0
@ -353,7 +256,7 @@
"losses_7d": 0, "losses_7d": 0,
"wins_30d": 0, "wins_30d": 0,
"losses_30d": 0, "losses_30d": 0,
"avg_return_1d": 0, "avg_return_1d": 1.36,
"avg_return_7d": 0, "avg_return_7d": 0,
"avg_return_30d": 0, "avg_return_30d": 0,
"win_rate_1d": 100.0 "win_rate_1d": 100.0
@ -361,15 +264,15 @@
}, },
"overall_1d": { "overall_1d": {
"count": 170, "count": 170,
"wins": 100, "wins": 94,
"avg_return": 0.95, "avg_return": 0.26,
"win_rate": 58.8 "win_rate": 55.3
}, },
"overall_7d": { "overall_7d": {
"count": 110, "count": 110,
"wins": 58, "wins": 56,
"avg_return": 0.45, "avg_return": -0.18,
"win_rate": 52.7 "win_rate": 50.9
}, },
"overall_30d": { "overall_30d": {
"count": 0, "count": 0,

View File

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

View File

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

View File

@ -29,30 +29,73 @@ logger = get_logger(__name__)
def load_recommendations() -> List[Dict[str, Any]]: 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" recommendations_dir = "data/recommendations"
if not os.path.exists(recommendations_dir): if not os.path.exists(recommendations_dir):
logger.warning(f"No recommendations directory found at {recommendations_dir}") logger.warning(f"No recommendations directory found at {recommendations_dir}")
return [] return []
all_recs = [] # Step 1: Load existing accumulated data from the performance database
pattern = os.path.join(recommendations_dir, "*.json") 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: try:
with open(filepath, "r") as f: with open(filepath, "r") as f:
data = json.load(f) data = json.load(f)
# Each file contains recommendations from one discovery run recs = data.get("recommendations", [])
recs = data.get("recommendations", []) run_date = data.get("date", basename.replace(".json", ""))
run_date = data.get("date", os.path.basename(filepath).replace(".json", "")) for rec in recs:
rec["discovery_date"] = run_date
for rec in recs: key = f"{rec.get('ticker')}|{run_date}"
rec["discovery_date"] = run_date if key not in existing:
all_recs.append(rec) existing[key] = rec
new_count += 1
except Exception as e: except Exception as e:
logger.error(f"Error loading {filepath}: {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]]: 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]): if not all([ticker, discovery_date, entry_price]):
continue continue
# Skip if already marked as closed
if rec.get("status") == "closed": if rec.get("status") == "closed":
continue continue
try: try:
# Get current price current_price = _parse_price(get_stock_price(ticker, curr_date=today))
current_price_data = get_stock_price(ticker, curr_date=today) if current_price is None:
logger.warning(f"Could not get price for {ticker}")
# 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}")
continue continue
# Calculate days since recommendation
rec_date = datetime.strptime(discovery_date, "%Y-%m-%d") rec_date = datetime.strptime(discovery_date, "%Y-%m-%d")
days_held = (datetime.now() - rec_date).days days_held = (datetime.now() - rec_date).days
# Calculate return
return_pct = ((current_price - entry_price) / entry_price) * 100 return_pct = ((current_price - entry_price) / entry_price) * 100
# Update metrics
rec["current_price"] = current_price rec["current_price"] = current_price
rec["return_pct"] = round(return_pct, 2) rec["return_pct"] = round(return_pct, 2)
rec["days_held"] = days_held rec["days_held"] = days_held
rec["last_updated"] = today 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: if days_held >= 7 and "return_7d" not in rec:
rec["return_7d"] = round(return_pct, 2) rec["return_7d"] = round(return_pct, 2)
rec["win_7d"] = return_pct > 0
if days_held >= 30 and "return_30d" not in rec: if days_held >= 30 and "return_30d" not in rec:
rec["return_30d"] = round(return_pct, 2) rec["return_30d"] = round(return_pct, 2)
rec["status"] = "closed" # Mark as complete after 30 days rec["win_30d"] = return_pct > 0
rec["status"] = "closed"
# 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
logger.info( logger.info(
f"{ticker}: Entry ${entry_price:.2f} → Current ${current_price:.2f} ({return_pct:+.1f}%) [{days_held}d]" 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]: def calculate_statistics(recommendations: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Calculate aggregate statistics from historical performance.""" """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 by strategy Delegates to DiscoveryAnalytics.calculate_statistics so there is a single
for rec in recommendations: source of truth for strategy normalization and metric calculation.
strategy = rec.get("strategy_match", "unknown") """
from tradingagents.dataflows.discovery.analytics import DiscoveryAnalytics
if strategy not in stats["by_strategy"]: analytics = DiscoveryAnalytics()
stats["by_strategy"][strategy] = { return analytics.calculate_statistics(recommendations)
"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
def print_statistics(stats: Dict[str, Any]): def print_statistics(stats: Dict[str, Any]):

View File

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

View File

@ -20,24 +20,40 @@ class DiscoveryAnalytics:
self.recommendations_dir = self.data_dir / "recommendations" self.recommendations_dir = self.data_dir / "recommendations"
self.recommendations_dir.mkdir(parents=True, exist_ok=True) self.recommendations_dir.mkdir(parents=True, exist_ok=True)
def update_performance_tracking(self): def _load_existing_database(self) -> Dict[str, Dict]:
"""Update performance metrics for all open recommendations.""" """Load existing performance database keyed by (ticker, discovery_date).
logger.info("📊 Updating recommendation performance tracking...")
if not self.recommendations_dir.exists(): Returns a dict mapping "TICKER|DATE" -> rec dict, preserving accumulated
logger.info("No historical recommendations to track yet.") return data (return_1d, return_7d, etc.) across runs.
return """
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 = [] all_recs = []
# Use glob directly on the path object if python 3.10+ otherwise str()
pattern = str(self.recommendations_dir / "*.json") pattern = str(self.recommendations_dir / "*.json")
for filepath in glob.glob(pattern): for filepath in glob.glob(pattern):
# Skip the database and stats files
if "performance_database" in filepath or "statistics" in filepath: if "performance_database" in filepath or "statistics" in filepath:
continue continue
try: try:
with open(filepath, "r") as f: with open(filepath, "r") as f:
data = json.load(f) data = json.load(f)
@ -49,16 +65,46 @@ class DiscoveryAnalytics:
all_recs.append(rec) all_recs.append(rec)
except Exception as e: except Exception as e:
logger.warning(f"Error loading {filepath}: {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.") logger.info("No recommendations found to track.")
return 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"] 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)...") 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") today = datetime.now().strftime("%Y-%m-%d")
updated_count = 0 updated_count = 0
@ -67,13 +113,10 @@ class DiscoveryAnalytics:
discovery_date = rec.get("discovery_date") discovery_date = rec.get("discovery_date")
entry_price = rec.get("entry_price") 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]): if rec.get("status") == "closed" or not all([ticker, discovery_date, entry_price]):
continue continue
try: 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 from tradingagents.dataflows.y_finance import get_stock_price
current_price = get_stock_price(ticker, curr_date=today) current_price = get_stock_price(ticker, curr_date=today)
@ -81,18 +124,16 @@ class DiscoveryAnalytics:
if current_price is None: if current_price is None:
continue continue
# Calculate metrics
rec_date = datetime.strptime(discovery_date, "%Y-%m-%d") rec_date = datetime.strptime(discovery_date, "%Y-%m-%d")
days_held = (datetime.now() - rec_date).days days_held = (datetime.now() - rec_date).days
return_pct = ((current_price - entry_price) / entry_price) * 100 return_pct = ((current_price - entry_price) / entry_price) * 100
# Update
rec["current_price"] = current_price rec["current_price"] = current_price
rec["return_pct"] = round(return_pct, 2) rec["return_pct"] = round(return_pct, 2)
rec["days_held"] = days_held rec["days_held"] = days_held
rec["last_updated"] = today 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: if days_held >= 1 and "return_1d" not in rec:
rec["return_1d"] = round(return_pct, 2) rec["return_1d"] = round(return_pct, 2)
rec["win_1d"] = return_pct > 0 rec["win_1d"] = return_pct > 0
@ -109,11 +150,11 @@ class DiscoveryAnalytics:
updated_count += 1 updated_count += 1
except Exception: except Exception:
# Silently skip errors to not interrupt discovery
pass pass
if updated_count > 0: # Step 4: Always save — even if no price updates, the merge may have added new recs
logger.info(f"Updated {updated_count} positions") 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) self._save_performance_db(all_recs)
else: else:
logger.info("No updates needed") logger.info("No updates needed")
@ -148,6 +189,35 @@ class DiscoveryAnalytics:
logger.info("💾 Updated performance database and statistics") 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: def calculate_statistics(self, recommendations: list) -> dict:
"""Calculate aggregate statistics from historical performance.""" """Calculate aggregate statistics from historical performance."""
stats = { stats = {
@ -158,12 +228,9 @@ class DiscoveryAnalytics:
"overall_30d": {"count": 0, "wins": 0, "avg_return": 0}, "overall_30d": {"count": 0, "wins": 0, "avg_return": 0},
} }
# Calculate by strategy def _get_strategy_bucket(strategy_name):
for rec in recommendations: if strategy_name not in stats["by_strategy"]:
strategy = rec.get("strategy_match", "unknown") stats["by_strategy"][strategy_name] = {
if strategy not in stats["by_strategy"]:
stats["by_strategy"][strategy] = {
"count": 0, "count": 0,
"wins_1d": 0, "wins_1d": 0,
"losses_1d": 0, "losses_1d": 0,
@ -175,45 +242,53 @@ class DiscoveryAnalytics:
"avg_return_7d": 0, "avg_return_7d": 0,
"avg_return_30d": 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 # 1-day stats
if "return_1d" in rec: if "return_1d" in rec:
stats["overall_1d"]["count"] += 1 stats["overall_1d"]["count"] += 1
bucket["avg_return_1d"] += rec["return_1d"]
if rec.get("win_1d"): if rec.get("win_1d"):
stats["overall_1d"]["wins"] += 1 stats["overall_1d"]["wins"] += 1
stats["by_strategy"][strategy]["wins_1d"] += 1 bucket["wins_1d"] += 1
else: else:
stats["by_strategy"][strategy]["losses_1d"] += 1 bucket["losses_1d"] += 1
stats["overall_1d"]["avg_return"] += rec["return_1d"] stats["overall_1d"]["avg_return"] += rec["return_1d"]
# 7-day stats # 7-day stats
if "return_7d" in rec: if "return_7d" in rec:
stats["overall_7d"]["count"] += 1 stats["overall_7d"]["count"] += 1
bucket["avg_return_7d"] += rec["return_7d"]
if rec.get("win_7d"): if rec.get("win_7d"):
stats["overall_7d"]["wins"] += 1 stats["overall_7d"]["wins"] += 1
stats["by_strategy"][strategy]["wins_7d"] += 1 bucket["wins_7d"] += 1
else: else:
stats["by_strategy"][strategy]["losses_7d"] += 1 bucket["losses_7d"] += 1
stats["overall_7d"]["avg_return"] += rec["return_7d"] stats["overall_7d"]["avg_return"] += rec["return_7d"]
# 30-day stats # 30-day stats
if "return_30d" in rec: if "return_30d" in rec:
stats["overall_30d"]["count"] += 1 stats["overall_30d"]["count"] += 1
bucket["avg_return_30d"] += rec["return_30d"]
if rec.get("win_30d"): if rec.get("win_30d"):
stats["overall_30d"]["wins"] += 1 stats["overall_30d"]["wins"] += 1
stats["by_strategy"][strategy]["wins_30d"] += 1 bucket["wins_30d"] += 1
else: else:
stats["by_strategy"][strategy]["losses_30d"] += 1 bucket["losses_30d"] += 1
stats["overall_30d"]["avg_return"] += rec["return_30d"] 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_1d"])
self._calculate_metric_averages(stats["overall_7d"]) self._calculate_metric_averages(stats["overall_7d"])
self._calculate_metric_averages(stats["overall_30d"]) 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(): for strategy, data in stats["by_strategy"].items():
total_1d = data["wins_1d"] + data["losses_1d"] total_1d = data["wins_1d"] + data["losses_1d"]
total_7d = data["wins_7d"] + data["losses_7d"] total_7d = data["wins_7d"] + data["losses_7d"]
@ -221,12 +296,15 @@ class DiscoveryAnalytics:
if total_1d > 0: if total_1d > 0:
data["win_rate_1d"] = round((data["wins_1d"] / total_1d) * 100, 1) 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: if total_7d > 0:
data["win_rate_7d"] = round((data["wins_7d"] / total_7d) * 100, 1) 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: if total_30d > 0:
data["win_rate_30d"] = round((data["wins_30d"] / total_30d) * 100, 1) 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 return stats

View File

@ -1,21 +1,23 @@
""" """
Main Streamlit app entry point for the Trading Agents Dashboard. Main Streamlit app entry point for the Trading Agents Dashboard.
This module sets up the dashboard page configuration, sidebar navigation, Dark terminal-inspired trading interface with sidebar navigation.
and routing to different pages based on user selection.
""" """
from datetime import datetime
import streamlit as st import streamlit as st
from tradingagents.ui import pages from tradingagents.ui import pages
from tradingagents.ui.theme import COLORS, GLOBAL_CSS
from tradingagents.ui.utils import load_quick_stats from tradingagents.ui.utils import load_quick_stats
def setup_page_config(): def setup_page_config():
"""Configure the Streamlit page settings.""" """Configure the Streamlit page settings."""
st.set_page_config( st.set_page_config(
page_title="Trading Agents Dashboard", page_title="Trading Agents",
page_icon="📊", page_icon="",
layout="wide", layout="wide",
initial_sidebar_state="expanded", initial_sidebar_state="expanded",
) )
@ -24,46 +26,101 @@ def setup_page_config():
def render_sidebar(): def render_sidebar():
"""Render the sidebar with navigation and quick stats.""" """Render the sidebar with navigation and quick stats."""
with st.sidebar: 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 # Navigation
st.markdown("### Navigation")
page = st.radio( page = st.radio(
"Select a page:", "Navigation",
options=["Home", "Today's Picks", "Portfolio", "Performance", "Settings"], options=["Overview", "Signals", "Portfolio", "Performance", "Config"],
label_visibility="collapsed", 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 # Quick stats
st.markdown("### Quick Stats")
try: try:
open_positions, win_rate = load_quick_stats() open_positions, win_rate = load_quick_stats()
st.markdown(
col1, col2 = st.columns(2) f"""
with col1: <div style="padding:0.75rem;background:{COLORS['bg_card']};
st.metric("Open Positions", open_positions) border:1px solid {COLORS['border']};border-radius:8px;">
with col2: <div style="font-family:'DM Sans',sans-serif;font-size:0.65rem;
st.metric("Win Rate", f"{win_rate:.1f}%") font-weight:600;text-transform:uppercase;letter-spacing:0.06em;
except Exception as e: color:{COLORS['text_muted']};margin-bottom:0.75rem;">
st.warning(f"Could not load quick stats: {str(e)}") 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 return page
def route_page(page): def route_page(page):
"""Route to the appropriate page based on selection.""" """Route to the appropriate page based on selection."""
if page == "Home": page_map = {
pages.home.render() "Overview": pages.home,
elif page == "Today's Picks": "Signals": pages.todays_picks,
pages.todays_picks.render() "Portfolio": pages.portfolio,
elif page == "Portfolio": "Performance": pages.performance,
pages.portfolio.render() "Config": pages.settings,
elif page == "Performance": }
pages.performance.render() module = page_map.get(page)
elif page == "Settings": if module:
pages.settings.render() module.render()
else: else:
st.error(f"Unknown page: {page}") st.error(f"Unknown page: {page}")
@ -72,17 +129,8 @@ def main():
"""Main entry point for the Streamlit app.""" """Main entry point for the Streamlit app."""
setup_page_config() setup_page_config()
# Custom CSS for better styling # Inject global theme CSS
st.markdown( st.markdown(GLOBAL_CSS, unsafe_allow_html=True)
"""
<style>
.main {
padding: 2rem;
}
</style>
""",
unsafe_allow_html=True,
)
# Render sidebar and get selected page # Render sidebar and get selected page
selected_page = render_sidebar() 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 Shows KPI cards, strategy scatter plot, and recent signal summary.
pipeline performance visualization.
""" """
from datetime import datetime
import pandas as pd import pandas as pd
import plotly.express as px import plotly.express as px
import streamlit as st 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: def render() -> None:
""" """Render the overview page."""
Render the home page with overview metrics and pipeline performance.
Displays: st.markdown(
- Dashboard title page_header("Overview", f"Market session {datetime.now().strftime('%A, %B %d %Y')}"),
- Warning if no statistics available unsafe_allow_html=True,
- 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")
# Load data
stats = load_statistics() stats = load_statistics()
positions = load_open_positions() positions = load_open_positions()
strategy_metrics = load_strategy_metrics() strategy_metrics = load_strategy_metrics()
# Check if statistics are available overall = stats.get("overall_7d", {}) if stats else {}
if not stats or not stats.get("overall_7d"): win_rate_7d = overall.get("win_rate", 0)
st.warning("No statistics data available. Run the discovery pipeline to generate data.") avg_return_7d = overall.get("avg_return", 0)
return total_recs = stats.get("total_recommendations", 0) if stats else 0
open_count = len(positions) if positions else 0
if not strategy_metrics: best_strat_name = "N/A"
st.warning("No strategy performance data available yet.") best_strat_wr = 0.0
return 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 # ---- KPI Row ----
overall_metrics = stats.get("overall_7d", {}) cols = st.columns(5)
win_rate_7d = overall_metrics.get("win_rate", 0) kpis = [
avg_return_7d = overall_metrics.get("avg_return", 0) ("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"),
open_positions_count = len(positions) if positions else 0 ("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 st.markdown("<div style='height:1.5rem;'></div>", unsafe_allow_html=True)
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}
# Display 4-column metric layout # ---- Two-column: strategy chart + today's signals ----
col1, col2, col3, col4 = st.columns(4) left_col, right_col = st.columns([3, 2])
with col1: with left_col:
st.metric( st.markdown(
label="Win Rate (7d)", '<div class="section-title">Strategy Performance <span class="accent">// scatter</span></div>',
value=f"{win_rate_7d:.1f}%", unsafe_allow_html=True,
delta=f"{win_rate_7d - 50:.1f}%" if win_rate_7d >= 50 else None,
) )
with col2: if strategy_metrics:
st.metric( df = pd.DataFrame(strategy_metrics)
label="Open Positions", template = get_plotly_template()
value=open_positions_count,
)
with col3: fig = px.scatter(
st.metric( df,
label="Avg Return (7d)", x="Win Rate",
value=f"{avg_return_7d:.2f}%", y="Avg Return",
delta=f"{avg_return_7d:.2f}%" if avg_return_7d > 0 else None, size="Count",
) color="Strategy",
hover_name="Strategy",
with col4: hover_data={"Win Rate": ":.1f", "Avg Return": ":.2f", "Count": True, "Strategy": False},
if best_strategy: labels={"Win Rate": "Win Rate (%)", "Avg Return": "Avg Return (%)"},
st.metric( size_max=40,
label="Best Strategy",
value=best_strategy["name"],
delta=f"{best_strategy['win_rate']:.1f}% WR",
) )
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: else:
st.metric( st.info("Run the discovery pipeline to generate strategy data.")
label="Best Strategy",
value="N/A",
)
# Strategy Performance scatter plot with right_col:
st.subheader("Strategy Performance") 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: if strategy_metrics:
df = pd.DataFrame(strategy_metrics) st.markdown("<div style='height:1rem;'></div>", unsafe_allow_html=True)
st.markdown(
# Create scatter plot with plotly '<div class="section-title">Strategy Breakdown <span class="accent">// table</span></div>',
fig = px.scatter( unsafe_allow_html=True,
df, )
x="Win Rate", df_table = pd.DataFrame(strategy_metrics).sort_values("Win Rate", ascending=False)
y="Avg Return", st.dataframe(
size="Count", df_table,
color="Strategy", width="stretch",
hover_name="Strategy", hide_index=True,
hover_data={ column_config={
"Win Rate": ":.1f", "Win Rate": st.column_config.NumberColumn(format="%.1f%%"),
"Avg Return": ":.2f", "Avg Return": st.column_config.NumberColumn(format="%+.2f%%"),
"Count": True, "Count": st.column_config.NumberColumn(format="%d"),
"Strategy": False,
},
title="Strategy Performance Analysis",
labels={
"Win Rate": "Win Rate (%)",
"Avg Return": "Avg Return (%)",
}, },
) )
# 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, Shows strategy scatter plot with themed Plotly charts, per-strategy
including win rates, average returns, and trading volume analysis. breakdown table, and win rate distribution.
""" """
import pandas as pd import pandas as pd
import plotly.express as px import plotly.express as px
import plotly.graph_objects as go
import streamlit as st 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: 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() strategy_metrics = load_strategy_metrics()
stats = load_statistics()
# Check if data is available
if not strategy_metrics: if not strategy_metrics:
st.warning( st.warning("No performance data available yet. Run the discovery pipeline and track outcomes.")
"No strategy performance data available. Run performance tracking to generate data."
)
return return
# Strategy Performance section template = get_plotly_template()
st.subheader("Strategy Performance") df = pd.DataFrame(strategy_metrics)
if strategy_metrics: # ---- Summary KPIs ----
df = pd.DataFrame(strategy_metrics) 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( fig = px.scatter(
df, df,
x="Win Rate", x="Win Rate",
@ -52,31 +75,99 @@ def render() -> None:
size="Count", size="Count",
color="Win Rate", color="Win Rate",
hover_name="Strategy", hover_name="Strategy",
hover_data={ hover_data={"Win Rate": ":.1f", "Avg Return": ":.2f", "Count": True},
"Win Rate": ":.1f", labels={"Win Rate": "Win Rate (%)", "Avg Return": "Avg Return (%)"},
"Avg Return": ":.2f", color_continuous_scale=[
"Count": True, [0, COLORS["red"]],
"Strategy": False, [0.5, COLORS["amber"]],
}, [1.0, COLORS["green"]],
title="Strategy Performance Analysis", ],
labels={ size_max=45,
"Win Rate": "Win Rate (%)",
"Avg Return": "Avg Return (%)",
},
color_continuous_scale="RdYlGn",
) )
# Add quadrant lines at y=0 (breakeven) and x=50 (50% win rate) fig.add_hline(y=0, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.4)
fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.5) fig.add_vline(x=50, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.4)
fig.add_vline(x=50, line_dash="dash", line_color="gray", opacity=0.5)
# Update layout for better visibility
fig.update_layout( fig.update_layout(
height=500, **template,
showlegend=True, height=400,
hovermode="closest", 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) df_sorted = df.sort_values("Win Rate", ascending=True)
else: colors = [COLORS["green"] if wr >= 50 else COLORS["red"] for wr in df_sorted["Win Rate"]]
st.info("No strategy data available for visualization.")
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 from datetime import datetime
import pandas as pd import pandas as pd
import streamlit as st import streamlit as st
from tradingagents.ui.theme import COLORS, kpi_card, page_header, pnl_color
from tradingagents.ui.utils import load_open_positions from tradingagents.ui.utils import load_open_positions
def render(): def render():
st.title("💼 Portfolio Tracker") st.markdown(page_header("Portfolio", "Open positions & P/L tracker"), unsafe_allow_html=True)
# Manual add form # ---- Add position form ----
with st.expander(" Add Position"): with st.expander("Add Position"):
col1, col2, col3, col4 = st.columns(4) col1, col2, col3, col4 = st.columns(4)
with col1: with col1:
ticker = st.text_input("Ticker") ticker = st.text_input("Ticker", placeholder="AAPL")
with col2: 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: with col3:
shares = st.number_input("Shares", min_value=0, step=1) shares = st.number_input("Shares", min_value=0, step=1)
with col4: with col4:
st.write("") # Spacing st.write("")
if st.button("Add"): if st.button("Add Position"):
if ticker and entry_price > 0 and shares > 0: if ticker and entry_price > 0 and shares > 0:
from tradingagents.dataflows.discovery.performance.position_tracker import ( from tradingagents.dataflows.discovery.performance.position_tracker import PositionTracker
PositionTracker,
)
tracker = PositionTracker() tracker = PositionTracker()
pos = tracker.create_position( pos = tracker.create_position({
{ "ticker": ticker.upper(),
"ticker": ticker.upper(), "entry_price": entry_price,
"entry_price": entry_price, "shares": shares,
"shares": shares, "recommendation_date": datetime.now().isoformat(),
"recommendation_date": datetime.now().isoformat(), "pipeline": "manual",
"pipeline": "manual", "scanner": "manual",
"scanner": "manual", "strategy_match": "manual",
"strategy_match": "manual", "confidence": 5,
"confidence": 5, })
}
)
tracker.save_position(pos) tracker.save_position(pos)
st.success(f"Added {ticker.upper()}") st.success(f"Added {ticker.upper()}")
st.rerun() st.rerun()
# Load positions
positions = load_open_positions() positions = load_open_positions()
if not 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 return
# Summary # ---- Portfolio summary ----
total_invested = sum(p["entry_price"] * p.get("shares", 0) for p in positions) 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_current = sum(p["metrics"]["current_price"] * p.get("shares", 0) for p in positions)
total_pnl = total_current - total_invested total_pnl = total_current - total_invested
total_pnl_pct = (total_pnl / total_invested * 100) if total_invested > 0 else 0 total_pnl_pct = (total_pnl / total_invested * 100) if total_invested > 0 else 0
col1, col2, col3, col4 = st.columns(4) cols = st.columns(4)
with col1: summary_kpis = [
st.metric("Invested", f"${total_invested:,.2f}") ("Invested", f"${total_invested:,.0f}", "", "blue"),
with col2: ("Current Value", f"${total_current:,.0f}", "", "blue"),
st.metric("Current", f"${total_current:,.2f}") ("P/L", f"${total_pnl:+,.0f}", f"{total_pnl_pct:+.1f}%", "green" if total_pnl >= 0 else "red"),
with col3: ("Positions", str(len(positions)), "", "amber"),
st.metric("P/L", f"${total_pnl:,.2f}", delta=f"{total_pnl_pct:+.1f}%") ]
with col4: for col, (label, value, delta, color) in zip(cols, summary_kpis):
st.metric("Positions", len(positions)) with col:
st.markdown(kpi_card(label, value, delta, color), unsafe_allow_html=True)
# Table st.markdown("<div style='height:1rem;'></div>", unsafe_allow_html=True)
st.subheader("📊 Positions")
# ---- 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: for p in positions:
pnl = (p["metrics"]["current_price"] - p["entry_price"]) * p.get("shares", 0) ticker = p["ticker"]
data.append( entry = p["entry_price"]
{ current = p["metrics"]["current_price"]
"Ticker": p["ticker"], shares = p.get("shares", 0)
"Entry": f"${p['entry_price']:.2f}", pnl_dollar = (current - entry) * shares
"Current": f"${p['metrics']['current_price']:.2f}", pnl_pct = p["metrics"]["current_return"]
"Shares": p.get("shares", 0), days = p["metrics"]["days_held"]
"P/L": f"${pnl:.2f}", color = pnl_color(pnl_pct)
"P/L %": f"{p['metrics']['current_return']:+.1f}%",
"Days": p["metrics"]["days_held"], 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) # ---- Data table fallback ----
st.dataframe(df, use_container_width=True) 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. Read-only view of scanners, pipelines, and data source configuration.
It provides a read-only view of current settings with expandable sections for detailed configuration.
""" """
import streamlit as st import streamlit as st
from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.ui.theme import COLORS, page_header
def render() -> None: def render() -> None:
""" """Render the configuration page."""
Render the settings 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 config = DEFAULT_CONFIG
discovery_config = config.get("discovery", {}) discovery_config = config.get("discovery", {})
# Display current configuration section # ---- Top-level settings ----
st.subheader("📋 Configuration") st.markdown(
'<div class="section-title">Discovery Settings <span class="accent">// core</span></div>',
unsafe_allow_html=True,
)
# Show key discovery settings settings_grid = [
col1, col2, col3 = st.columns(3) ("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: cols = st.columns(len(settings_grid))
st.metric( for col, (label, val) in zip(cols, settings_grid):
label="Discovery Mode", with col:
value=discovery_config.get("discovery_mode", "N/A"), 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: pipelines = discovery_config.get("pipelines", {})
st.metric( for name, cfg in pipelines.items():
label="Max Candidates", enabled = cfg.get("enabled", False)
value=discovery_config.get("max_candidates_to_analyze", "N/A"), 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: scanners = discovery_config.get("scanners", {})
st.metric( for name, cfg in scanners.items():
label="Final Recommendations", enabled = cfg.get("enabled", False)
value=discovery_config.get("final_recommendations", "N/A"), 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.markdown(
st.subheader("🔄 Pipelines") 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", {}) # ---- Data Sources ----
st.markdown("<div style='height:1.5rem;'></div>", unsafe_allow_html=True)
if pipelines: st.markdown(
for pipeline_name, pipeline_config in pipelines.items(): '<div class="section-title">Data Sources <span class="accent">// vendors</span></div>',
with st.expander( unsafe_allow_html=True,
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_vendors = config.get("data_vendors", {}) data_vendors = config.get("data_vendors", {})
if data_vendors: if data_vendors:
for vendor_type, vendor_name in data_vendors.items(): cols = st.columns(3)
st.write(f"**{vendor_type.replace('_', ' ').title()}**: {vendor_name}") 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: 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 from datetime import datetime
@ -6,6 +11,7 @@ import pandas as pd
import plotly.express as px import plotly.express as px
import streamlit as st import streamlit as st
from tradingagents.ui.theme import COLORS, get_plotly_template, page_header, signal_card
from tradingagents.ui.utils import load_recommendations from tradingagents.ui.utils import load_recommendations
@ -17,13 +23,8 @@ def _load_price_history(ticker: str, period: str) -> pd.DataFrame:
return pd.DataFrame() return pd.DataFrame()
data = download_history( data = download_history(
ticker, ticker, period=period, interval="1d", auto_adjust=True, progress=False,
period=period,
interval="1d",
auto_adjust=True,
progress=False,
) )
if data is None or data.empty: if data is None or data.empty:
return pd.DataFrame() return pd.DataFrame()
@ -42,100 +43,116 @@ def _load_price_history(ticker: str, period: str) -> pd.DataFrame:
def render(): def render():
st.title("📋 Today's Recommendations")
today = datetime.now().strftime("%Y-%m-%d") today = datetime.now().strftime("%Y-%m-%d")
recommendations, meta = load_recommendations(today, return_meta=True) 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: if not recommendations:
st.warning(f"No recommendations for {today}") st.warning(f"No recommendations for {today}.")
return return
if meta.get("is_fallback") and meta.get("date"): 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) # ---- Controls row ----
chart_window = st.selectbox( ctrl_cols = st.columns([1, 1, 1, 1])
"Price history window", with ctrl_cols[0]:
["1mo", "3mo", "6mo", "1y"], pipelines = sorted(set(
index=1, (r.get("pipeline") or r.get("strategy_match") or "unknown") for r in recommendations
) ))
pipeline_filter = st.multiselect("Strategy", pipelines, default=pipelines)
# Filters with ctrl_cols[1]:
col1, col2, col3 = st.columns(3) min_confidence = st.slider("Min Confidence", 1, 10, 1)
with col1: with ctrl_cols[2]:
pipelines = list( min_score = st.slider("Min Score", 0, 100, 0)
set( with ctrl_cols[3]:
(r.get("pipeline") or r.get("strategy_match") or "unknown") for r in recommendations show_charts = st.checkbox("Price Charts", value=False)
) if show_charts:
) chart_window = st.selectbox("Window", ["1mo", "3mo", "6mo", "1y"], index=1)
pipeline_filter = st.multiselect("Pipeline", pipelines, default=pipelines) else:
with col2: chart_window = "3mo"
min_confidence = st.slider("Min Confidence", 1, 10, 7)
with col3:
min_score = st.slider("Min Score", 0, 100, 70)
# Apply filters # Apply filters
filtered = [ filtered = [
r r for r in recommendations
for r in recommendations
if (r.get("pipeline") or r.get("strategy_match") or "unknown") in pipeline_filter if (r.get("pipeline") or r.get("strategy_match") or "unknown") in pipeline_filter
and r.get("confidence", 0) >= min_confidence and r.get("confidence", 0) >= min_confidence
and r.get("final_score", 0) >= min_score 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 # ---- Signal cards in 2-column grid ----
for i, rec in enumerate(filtered, 1): for i in range(0, len(filtered), 2):
ticker = rec.get("ticker", "UNKNOWN") cols = st.columns(2)
score = rec.get("final_score", 0) for j, col in enumerate(cols):
confidence = rec.get("confidence", 0) idx = i + j
pipeline = (rec.get("pipeline") or rec.get("strategy_match") or "unknown").title() if idx >= len(filtered):
scanner = rec.get("scanner") or rec.get("strategy_match") or "unknown" break
entry_price = rec.get("entry_price") rec = filtered[idx]
current_price = rec.get("current_price") 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( with col:
f"#{i} {ticker} - {rec.get('company_name', '')} (Score: {score}, Conf: {confidence}/10)" st.markdown(
): signal_card(rank, ticker, score, confidence, strategy, entry_price, reason),
col1, col2 = st.columns([2, 1]) 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: if show_charts:
history = _load_price_history(ticker, chart_window) history = _load_price_history(ticker, chart_window)
if history.empty: if not history.empty:
st.caption("Price history unavailable.") template = get_plotly_template()
else: fig = px.line(history, x="date", y="close", labels={"date": "", "close": "Price"})
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)
with col2: # Color line green if trending up, red if down
if st.button("✅ Enter Position", key=f"enter_{ticker}"): first_close = history["close"].iloc[0]
st.info("Position entry modal (TODO)") last_close = history["close"].iloc[-1]
if st.button("👀 Watch", key=f"watch_{ticker}"): line_color = COLORS["green"] if last_close >= first_close else COLORS["red"]
st.success(f"Added {ticker} to watchlist")
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")