Add recommendations folder so that the UI can display it 4
This commit is contained in:
parent
0bc7dda086
commit
8ebb42114d
File diff suppressed because it is too large
Load Diff
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -27,11 +27,13 @@ logger = get_logger(__name__)
|
|||
|
||||
|
||||
def main():
|
||||
logger.info("""
|
||||
logger.info(
|
||||
"""
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ TradingAgents - Historical Memory Builder ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Configuration
|
||||
tickers = [
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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]):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 — {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()
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 · {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"),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue