Spaces:
Running
Running
| from __future__ import annotations | |
| from collections import Counter, defaultdict | |
| from datetime import date, datetime | |
| import hashlib | |
| import json | |
| from statistics import mean, median, pstdev | |
| from sqlalchemy import desc, func, or_, select | |
| from sqlalchemy.orm import Session | |
| from app.core.config import get_settings | |
| from app.models import ( | |
| Asset, | |
| BlumTradingPowerScore, | |
| LearningBenchmarkComparison, | |
| LearningProgressSnapshot, | |
| LearningRun, | |
| LearningStrengthWeaknessMap, | |
| LiveForwardPaperTrade, | |
| PriceHistory, | |
| SelfImprovementAction, | |
| TradingGame, | |
| TradingGameTrade, | |
| ) | |
| from app.services.trade_transparency import TradeLedgerService, TradingGameRealityCheckService, clamp, safe_float | |
| from app.services.trading_intelligence_lab import ( | |
| HistoricalLiveComparisonService, | |
| LAB_POLICY, | |
| TradingCapitalCycleService, | |
| TradingIntelligenceMetricsService, | |
| cycle_stats, | |
| executable_trades, | |
| metric_payload, | |
| sample_context, | |
| ) | |
| settings = get_settings() | |
| LEARNING_INTELLIGENCE_POLICY = ( | |
| "Learning Intelligence is a benchmark-aware research dashboard. It must expose weakness, sample-size limits " | |
| "and benchmark underperformance instead of presenting simulated P/L as proof." | |
| ) | |
| MARKET_BENCHMARKS = ["SPY", "QQQ", "VTI", "DIA", "IWM"] | |
| SECTOR_BENCHMARKS = ["XLK", "XLF", "XLV", "XLY", "XLE", "XLI", "XLP", "XLU", "XLC", "XLB", "XLRE"] | |
| BASELINE_BENCHMARKS = [ | |
| ("cash_no_trade_baseline", "baseline"), | |
| ("random_asset_selection_proxy", "baseline"), | |
| ("random_entry_exit_proxy", "baseline"), | |
| ("momentum_baseline_proxy", "baseline"), | |
| ("moving_average_crossover_proxy", "baseline"), | |
| ("sector_rotation_proxy", "baseline"), | |
| ] | |
| class BlumTradingPowerScoreService: | |
| """Strict composite score for current BLUM trading intelligence evidence.""" | |
| def get(self, db: Session) -> dict: | |
| return self.calculate(db, persist=False) | |
| def recalculate(self, db: Session) -> dict: | |
| return self.calculate(db, persist=True) | |
| def persist_if_evidence_changed(self, db: Session) -> dict: | |
| """Persist one score projection for each distinct productive evidence state.""" | |
| source = self._evidence_source(db) | |
| if source is None: | |
| return {"status": "skipped", "reason": "no_productive_learning_evidence"} | |
| fingerprint = hashlib.sha256( | |
| json.dumps(source, sort_keys=True, separators=(",", ":")).encode("utf-8") | |
| ).hexdigest() | |
| latest = db.scalar( | |
| select(BlumTradingPowerScore) | |
| .order_by(desc(BlumTradingPowerScore.calculated_at), desc(BlumTradingPowerScore.id)) | |
| .limit(1) | |
| ) | |
| latest_fingerprint = (latest.warnings_json or {}).get("evidence_fingerprint") if latest else None | |
| if latest_fingerprint == fingerprint: | |
| return { | |
| "status": "unchanged", | |
| "reason": "evidence_state_already_projected", | |
| "row_id": latest.id, | |
| "evidence_fingerprint": fingerprint, | |
| } | |
| payload = self.calculate(db, persist=True) | |
| row = db.get(BlumTradingPowerScore, payload["row_id"]) | |
| row.warnings_json = { | |
| **(row.warnings_json or {}), | |
| "evidence_fingerprint": fingerprint, | |
| "evidence_source": source, | |
| } | |
| db.commit() | |
| return { | |
| **payload, | |
| "status": "persisted", | |
| "evidence_fingerprint": fingerprint, | |
| "evidence_source": source, | |
| } | |
| def _evidence_source(self, db: Session) -> dict | None: | |
| productive_run = db.scalar( | |
| select(LearningRun) | |
| .where( | |
| or_( | |
| LearningRun.predictions_created > 0, | |
| LearningRun.outcomes_evaluated > 0, | |
| LearningRun.memory_updates > 0, | |
| ) | |
| ) | |
| .order_by(desc(LearningRun.started_at), desc(LearningRun.id)) | |
| .limit(1) | |
| ) | |
| if productive_run is None: | |
| return None | |
| closed_statuses = ("CLOSED", "EXITED", "EXPIRED", "INVALIDATED") | |
| last_closed_at = db.scalar( | |
| select(func.max(LiveForwardPaperTrade.closed_at)).where( | |
| LiveForwardPaperTrade.status.in_(closed_statuses) | |
| ) | |
| ) | |
| return { | |
| "learning_run_pk": productive_run.id, | |
| "learning_run_id": productive_run.run_id, | |
| "predictions_created": int(productive_run.predictions_created or 0), | |
| "outcomes_evaluated": int(productive_run.outcomes_evaluated or 0), | |
| "memory_updates": int(productive_run.memory_updates or 0), | |
| "historical_trade_count": int(db.scalar(select(func.count(TradingGameTrade.id))) or 0), | |
| "paper_forward_closed_count": int( | |
| db.scalar( | |
| select(func.count(LiveForwardPaperTrade.id)).where( | |
| LiveForwardPaperTrade.status.in_(closed_statuses) | |
| ) | |
| ) | |
| or 0 | |
| ), | |
| "paper_forward_last_closed_at": last_closed_at.isoformat() if last_closed_at else None, | |
| } | |
| def calculate(self, db: Session, persist: bool = False) -> dict: | |
| game = latest_trading_game(db) | |
| rows = game_trades(db, game.id if game else None) | |
| metrics = metric_payload(rows, scope="game", scope_id=str(game.id) if game else None, window_type="all", window_size=None) | |
| live_metrics = HistoricalLiveComparisonService().compare(db).get("live") or {} | |
| cycles = db.scalars(select_by_game_cycle(game.id if game else None)).all() if game else [] | |
| cycle_payload = cycle_stats(cycles) | |
| reality = TradingGameRealityCheckService().evaluate(db, game.id if game else None, persist=False) if game else {} | |
| benchmark_rows = BenchmarkComparisonService().comparisons(db, persist=False).get("rows", []) | |
| progress = LearningProgressEvaluator().overview(db, persist=False) | |
| components = trading_power_components(rows, metrics, live_metrics, cycle_payload, reality, benchmark_rows, progress) | |
| score = trading_power_score(components) | |
| classification = classify_trading_power_score(score) | |
| warnings = trading_power_warnings(metrics, live_metrics, cycle_payload, reality, benchmark_rows, rows) | |
| explanation = trading_power_explanation(score, classification, components, warnings, metrics, benchmark_rows) | |
| truth = truth_panel_from_payload(score, classification, warnings, benchmark_rows, metrics, live_metrics) | |
| payload = { | |
| "status": "ok" if game else "no_game", | |
| "calculated_at": datetime.utcnow().isoformat(), | |
| "mode": "historical_plus_live", | |
| "scope": "global", | |
| "score": round(score, 2), | |
| "classification": classification, | |
| "components": components, | |
| "warnings": warnings, | |
| "truth_panel": truth, | |
| "sample_size": metrics.get("trades_count", 0), | |
| "live_sample_size": live_metrics.get("trades_count", 0), | |
| "statistical_confidence": statistical_confidence_label(metrics.get("trades_count", 0), live_metrics.get("trades_count", 0), sample_context(rows)), | |
| "explanation": explanation, | |
| "policy": LEARNING_INTELLIGENCE_POLICY, | |
| } | |
| if persist: | |
| row = BlumTradingPowerScore( | |
| mode=payload["mode"], | |
| scope=payload["scope"], | |
| score=payload["score"], | |
| classification=classification, | |
| benchmark_relative_score=components["benchmark_relative_score"], | |
| expectancy_score=components["expectancy_score"], | |
| drawdown_control_score=components["drawdown_control_score"], | |
| win_loss_quality_score=components["win_loss_quality_score"], | |
| missed_entry_penalty=components["missed_entry_penalty"], | |
| risk_management_score=components["risk_management_score"], | |
| capital_cycle_score=components["capital_cycle_score"], | |
| live_forward_validation_score=components["live_forward_validation_score"], | |
| regime_robustness_score=components["regime_robustness_score"], | |
| setup_diversity_score=components["setup_diversity_score"], | |
| statistical_confidence_score=components["statistical_confidence_score"], | |
| reproducibility_score=components["reproducibility_score"], | |
| decision_quality_score=components["decision_quality_score"], | |
| learning_velocity_score=components["learning_velocity_score"], | |
| explanation=explanation, | |
| warnings_json={"warnings": warnings, "truth_panel": truth}, | |
| ) | |
| db.add(row) | |
| db.commit() | |
| payload["row_id"] = row.id | |
| return payload | |
| class BenchmarkComparisonService: | |
| """Compares BLUM against market benchmarks and simple internal baselines.""" | |
| def comparisons(self, db: Session, persist: bool = False) -> dict: | |
| game = latest_trading_game(db) | |
| rows = game_trades(db, game.id if game else None) | |
| closed = executable_trades(rows) | |
| output = [] | |
| for name in MARKET_BENCHMARKS: | |
| output.append(self.compare_to_benchmark(db, name, "market", closed)) | |
| for name in SECTOR_BENCHMARKS: | |
| output.append(self.compare_to_benchmark(db, name, "sector", closed)) | |
| for name, kind in BASELINE_BENCHMARKS: | |
| output.append(self.compare_to_baseline(name, kind, closed)) | |
| if persist: | |
| for item in output: | |
| db.add( | |
| LearningBenchmarkComparison( | |
| mode=item["mode"], | |
| benchmark_name=item["benchmark_name"], | |
| benchmark_type=item["benchmark_type"], | |
| period_start=parse_date_value(item.get("period_start")), | |
| period_end=parse_date_value(item.get("period_end")), | |
| blum_return=item.get("blum_return"), | |
| benchmark_return=item.get("benchmark_return"), | |
| excess_return=item.get("excess_return"), | |
| blum_max_drawdown=item.get("blum_max_drawdown"), | |
| benchmark_max_drawdown=item.get("benchmark_max_drawdown"), | |
| blum_volatility=item.get("blum_volatility"), | |
| benchmark_volatility=item.get("benchmark_volatility"), | |
| sharpe_proxy=item.get("sharpe_proxy"), | |
| sortino_proxy=item.get("sortino_proxy"), | |
| calmar_proxy=item.get("calmar_proxy"), | |
| information_ratio_proxy=item.get("information_ratio_proxy"), | |
| hit_rate_vs_benchmark=item.get("hit_rate_vs_benchmark"), | |
| risk_adjusted_advantage=item.get("risk_adjusted_advantage"), | |
| sample_size=item.get("sample_size", 0), | |
| statistical_confidence=item.get("statistical_confidence", "very low evidence"), | |
| result_label=item.get("result_label", "insufficient_sample"), | |
| explanation=item.get("explanation", ""), | |
| ) | |
| ) | |
| db.commit() | |
| return {"status": "ok", "rows": output, "policy": LEARNING_INTELLIGENCE_POLICY} | |
| def detail(self, db: Session, benchmark_name: str) -> dict: | |
| rows = self.comparisons(db, persist=False).get("rows", []) | |
| needle = benchmark_name.upper() | |
| row = next((item for item in rows if item["benchmark_name"].upper() == needle), None) | |
| return {"status": "ok" if row else "not_found", "benchmark": row, "policy": LEARNING_INTELLIGENCE_POLICY} | |
| def compare_to_benchmark(self, db: Session, benchmark: str, benchmark_type: str, rows: list[TradingGameTrade]) -> dict: | |
| returns = trade_returns(rows) | |
| benchmark_returns = benchmark_returns_for_rows(db, rows, benchmark) | |
| sample = min(len(returns), len(benchmark_returns)) if benchmark_returns else len(returns) | |
| blum_return = mean(returns) if returns else None | |
| benchmark_return = mean(benchmark_returns) if benchmark_returns else price_period_return(db, benchmark, rows) | |
| excess = blum_return - benchmark_return if blum_return is not None and benchmark_return is not None else None | |
| hit_rate = hit_rate_vs_benchmark(returns, benchmark_returns) | |
| label = benchmark_result_label(excess, sample) | |
| confidence = statistical_confidence_label(sample, 0, sample_context(rows)) | |
| explanation = benchmark_explanation(benchmark, benchmark_type, sample, blum_return, benchmark_return, excess, label, benchmark_returns) | |
| return { | |
| "mode": "historical_simulation", | |
| "benchmark_name": benchmark, | |
| "benchmark_type": benchmark_type, | |
| "period_start": first_date(rows), | |
| "period_end": last_date(rows), | |
| "blum_return": round_or_none(blum_return), | |
| "benchmark_return": round_or_none(benchmark_return), | |
| "excess_return": round_or_none(excess), | |
| "blum_max_drawdown": round_or_none(min(returns) if returns else None), | |
| "benchmark_max_drawdown": round_or_none(min(benchmark_returns) if benchmark_returns else None), | |
| "blum_volatility": round_or_none(pstdev(returns) if len(returns) > 1 else None), | |
| "benchmark_volatility": round_or_none(pstdev(benchmark_returns) if len(benchmark_returns) > 1 else None), | |
| "sharpe_proxy": ratio_or_none(blum_return, pstdev(returns) if len(returns) > 1 else None), | |
| "sortino_proxy": ratio_or_none(blum_return, downside_volatility(returns)), | |
| "calmar_proxy": ratio_or_none(blum_return, abs(min(returns)) if returns else None), | |
| "information_ratio_proxy": ratio_or_none(excess, pstdev([a - b for a, b in zip(returns, benchmark_returns)]) if len(benchmark_returns) > 1 else None), | |
| "hit_rate_vs_benchmark": round_or_none(hit_rate), | |
| "risk_adjusted_advantage": round_or_none((excess or 0) - abs((min(returns) if returns else 0) - (min(benchmark_returns) if benchmark_returns else 0)) * 0.25 if excess is not None else None), | |
| "sample_size": sample, | |
| "statistical_confidence": confidence, | |
| "result_label": label, | |
| "explanation": explanation, | |
| } | |
| def compare_to_baseline(self, name: str, benchmark_type: str, rows: list[TradingGameTrade]) -> dict: | |
| returns = trade_returns(rows) | |
| benchmark = baseline_return(name, rows) | |
| sample = len(returns) | |
| blum_return = mean(returns) if returns else None | |
| excess = blum_return - benchmark if blum_return is not None and benchmark is not None else None | |
| label = benchmark_result_label(excess, sample) | |
| return { | |
| "mode": "historical_simulation", | |
| "benchmark_name": name, | |
| "benchmark_type": benchmark_type, | |
| "period_start": first_date(rows), | |
| "period_end": last_date(rows), | |
| "blum_return": round_or_none(blum_return), | |
| "benchmark_return": round_or_none(benchmark), | |
| "excess_return": round_or_none(excess), | |
| "blum_max_drawdown": round_or_none(min(returns) if returns else None), | |
| "benchmark_max_drawdown": None, | |
| "blum_volatility": round_or_none(pstdev(returns) if len(returns) > 1 else None), | |
| "benchmark_volatility": None, | |
| "sharpe_proxy": ratio_or_none(blum_return, pstdev(returns) if len(returns) > 1 else None), | |
| "sortino_proxy": ratio_or_none(blum_return, downside_volatility(returns)), | |
| "calmar_proxy": ratio_or_none(blum_return, abs(min(returns)) if returns else None), | |
| "information_ratio_proxy": None, | |
| "hit_rate_vs_benchmark": round_or_none(sum(1 for value in returns if benchmark is not None and value > benchmark) / max(1, len(returns)) if returns else None), | |
| "risk_adjusted_advantage": round_or_none(excess), | |
| "sample_size": sample, | |
| "statistical_confidence": statistical_confidence_label(sample, 0, sample_context(rows)), | |
| "result_label": label, | |
| "explanation": baseline_explanation(name, sample, benchmark, excess, label), | |
| } | |
| class LearningProgressEvaluator: | |
| """Measures rolling improvement, deterioration and inconclusive zones.""" | |
| def overview(self, db: Session, persist: bool = False) -> dict: | |
| game = latest_trading_game(db) | |
| rows = game_trades(db, game.id if game else None) | |
| windows = self.rolling(db).get("windows", []) | |
| all_metric = metric_payload(rows, "game", str(game.id) if game else None, "all", None) | |
| trend = progress_trend_label(windows) | |
| score = intelligence_growth_score(windows, all_metric) | |
| payload = { | |
| "status": "ok" if game else "no_game", | |
| "summary": progress_explanation(trend, score, windows, all_metric), | |
| "trend_label": trend, | |
| "intelligence_growth_score": round(score, 2), | |
| "current": all_metric, | |
| "rolling": windows, | |
| "policy": LEARNING_INTELLIGENCE_POLICY, | |
| } | |
| if persist: | |
| db.add(progress_snapshot_row(all_metric, trend, score, None)) | |
| for item in windows: | |
| db.add(progress_snapshot_row(item, progress_trend_label([item]), safe_float(item.get("intelligence_growth_score")), item.get("window_size"))) | |
| db.commit() | |
| return payload | |
| def rolling(self, db: Session) -> dict: | |
| game = latest_trading_game(db) | |
| rows = game_trades(db, game.id if game else None) | |
| payloads = [] | |
| for window in (30, 100, 250): | |
| payloads.append(metric_payload(rows[-window:], "game", str(game.id) if game else None, "rolling", window)) | |
| return {"status": "ok" if game else "no_game", "windows": payloads, "policy": LEARNING_INTELLIGENCE_POLICY} | |
| def by_dimension(self, db: Session, dimension: str) -> dict: | |
| service = TradingIntelligenceMetricsService() | |
| if dimension == "setup": | |
| return service.by_dimension(db, "setup") | |
| if dimension == "regime": | |
| return service.by_dimension(db, "regime") | |
| return {"status": "not_supported", "dimension": dimension, "rows": []} | |
| class LearningWeaknessMapService: | |
| """Finds where BLUM is strong, weak or statistically under-covered.""" | |
| def map(self, db: Session, dimension: str | None = None, persist: bool = False) -> dict: | |
| dimensions = [dimension] if dimension else ["setup", "regime", "sector", "engine"] | |
| rows: list[dict] = [] | |
| for dim in dimensions: | |
| rows.extend(self.dimension_rows(db, dim)) | |
| rows.sort(key=lambda item: (priority_rank(item["priority"]), item["weakness_score"]), reverse=True) | |
| if persist: | |
| for item in rows: | |
| db.add( | |
| LearningStrengthWeaknessMap( | |
| dimension=item["dimension"], | |
| entity=item["entity"], | |
| strength_score=item["strength_score"], | |
| weakness_score=item["weakness_score"], | |
| sample_size=item["sample_size"], | |
| evidence=item["evidence"], | |
| main_problem=item["main_problem"], | |
| recommended_action=item["recommended_action"], | |
| priority=item["priority"], | |
| status=item["status"], | |
| ) | |
| ) | |
| db.commit() | |
| return {"status": "ok", "dimension": dimension or "all", "rows": rows, "policy": LEARNING_INTELLIGENCE_POLICY} | |
| def dimension_rows(self, db: Session, dimension: str) -> list[dict]: | |
| if dimension in {"setup", "regime", "sector"}: | |
| metrics = TradingIntelligenceMetricsService().by_dimension(db, dimension).get("rows", []) | |
| return [weakness_from_metric(dimension, row) for row in metrics] | |
| return self.engine_rows(db) | |
| def engine_rows(self, db: Session) -> list[dict]: | |
| from app.models import TradeEngineAttribution | |
| attributions = db.scalars(select(TradeEngineAttribution)).all() | |
| grouped: dict[str, list] = defaultdict(list) | |
| for row in attributions: | |
| grouped[row.engine_name or "unknown"].append(row) | |
| output = [] | |
| for engine, items in grouped.items(): | |
| sample = len(items) | |
| correct = sum(1 for item in items if item.was_correct) | |
| avg_quality = mean([safe_float(item.evidence_quality) for item in items]) if items else 0 | |
| strength = clamp((correct / max(1, sample)) * 70 + avg_quality * 0.3) | |
| weakness = clamp(100 - strength + (30 if sample < settings.self_improvement_min_sample_size else 0)) | |
| output.append( | |
| { | |
| "dimension": "engine", | |
| "entity": engine, | |
| "strength_score": round(strength, 2), | |
| "weakness_score": round(weakness, 2), | |
| "sample_size": sample, | |
| "evidence": {"correct": correct, "average_evidence_quality": round(avg_quality, 2)}, | |
| "main_problem": "Engine evidence is statistically thin." if sample < settings.self_improvement_min_sample_size else "Engine attribution quality needs monitoring.", | |
| "recommended_action": "Collect more attributed trades before changing engine weights." if sample < settings.self_improvement_min_sample_size else "Compare this engine against benchmark-relative outcomes before weight changes.", | |
| "priority": "high" if weakness >= 70 else "medium" if weakness >= 45 else "low", | |
| "status": "open", | |
| } | |
| ) | |
| return output | |
| class SelfImprovementActionEngine: | |
| """Converts measured weakness into auditable, reversible improvement proposals.""" | |
| def list(self, db: Session, limit: int = 80) -> dict: | |
| rows = db.scalars(select(SelfImprovementAction).order_by(desc(SelfImprovementAction.created_at)).limit(limit)).all() | |
| if not rows: | |
| generated = self.generate(db, persist=False).get("actions", []) | |
| return {"status": "preview", "actions": generated[:limit], "policy": LEARNING_INTELLIGENCE_POLICY} | |
| return {"status": "ok", "actions": [serialize_action(row) for row in rows], "policy": LEARNING_INTELLIGENCE_POLICY} | |
| def generate(self, db: Session, persist: bool = True) -> dict: | |
| weakness_rows = LearningWeaknessMapService().map(db, persist=False).get("rows", []) | |
| actions = [self.action_from_weakness(row) for row in weakness_rows if row["weakness_score"] >= 45] | |
| actions = dedupe_actions(actions) | |
| if persist: | |
| existing = { | |
| (row.source_dimension, row.detected_problem, row.affected_module, row.status) | |
| for row in db.scalars(select(SelfImprovementAction).where(SelfImprovementAction.status.in_(["proposed", "testing", "applied"]))).all() | |
| } | |
| inserted = [] | |
| for action in actions: | |
| key = (action["source_dimension"], action["detected_problem"], action["affected_module"], action["status"]) | |
| if key in existing: | |
| continue | |
| row = SelfImprovementAction(**action) | |
| db.add(row) | |
| inserted.append(row) | |
| db.commit() | |
| actions = [serialize_action(row) for row in inserted] or actions | |
| return { | |
| "status": "ok", | |
| "actions": actions, | |
| "auto_apply": { | |
| "enabled": settings.self_improvement_enabled, | |
| "global_auto_apply": settings.self_improvement_auto_apply, | |
| "low_risk_auto_apply": settings.self_improvement_auto_apply_low_risk, | |
| "min_sample_size": settings.self_improvement_min_sample_size, | |
| "policy": "No source code self-modification. Actions are auditable parameter or sampling proposals.", | |
| }, | |
| "policy": LEARNING_INTELLIGENCE_POLICY, | |
| } | |
| def apply(self, db: Session, action_id: int) -> dict: | |
| row = db.get(SelfImprovementAction, action_id) | |
| if not row: | |
| return {"status": "not_found", "action_id": action_id} | |
| if not settings.self_improvement_enabled: | |
| return {"status": "disabled", "action": serialize_action(row), "reason": "SELF_IMPROVEMENT_ENABLED=false"} | |
| if not settings.self_improvement_auto_apply and not (settings.self_improvement_auto_apply_low_risk and row.priority == "low"): | |
| row.status = "testing" | |
| row.notes_json = { | |
| **(row.notes_json or {}), | |
| "applied": False, | |
| "reason": "Auto-apply is disabled. Action moved to testing queue for human review.", | |
| "reversible": True, | |
| } | |
| db.commit() | |
| return {"status": "testing_only", "action": serialize_action(row), "policy": LEARNING_INTELLIGENCE_POLICY} | |
| row.status = "applied" | |
| row.applied_at = datetime.utcnow() | |
| row.notes_json = {**(row.notes_json or {}), "applied": True, "reversible": True, "source_code_modified": False} | |
| db.commit() | |
| return {"status": "applied", "action": serialize_action(row), "policy": LEARNING_INTELLIGENCE_POLICY} | |
| def evaluate(self, db: Session, action_id: int) -> dict: | |
| row = db.get(SelfImprovementAction, action_id) | |
| if not row: | |
| return {"status": "not_found", "action_id": action_id} | |
| before = row.before_metric | |
| after = latest_metric_for_action(db, row) | |
| row.after_metric = after | |
| row.improvement_observed = after is not None and before is not None and after > before | |
| if row.improvement_observed is False and settings.self_improvement_rollback_enabled: | |
| row.status = "retired" | |
| elif row.improvement_observed: | |
| row.status = "applied" | |
| db.commit() | |
| return {"status": "ok", "action": serialize_action(row), "policy": LEARNING_INTELLIGENCE_POLICY} | |
| def action_from_weakness(self, weakness: dict) -> dict: | |
| action = self_improvement_action_from_weakness(weakness) | |
| return action | |
| class LearningIntelligenceDashboardService: | |
| """Single control-room payload for the frontend and chat.""" | |
| def dashboard(self, db: Session) -> dict: | |
| trading_power = BlumTradingPowerScoreService().get(db) | |
| benchmarks = BenchmarkComparisonService().comparisons(db, persist=False) | |
| progress = LearningProgressEvaluator().overview(db, persist=False) | |
| weakness = LearningWeaknessMapService().map(db, persist=False) | |
| actions = SelfImprovementActionEngine().list(db, limit=40) | |
| live = HistoricalLiveComparisonService().compare(db) | |
| return { | |
| "status": "ok", | |
| "generated_at": datetime.utcnow().isoformat(), | |
| "trading_power": trading_power, | |
| "benchmarks": benchmarks, | |
| "progress": progress, | |
| "weakness_map": weakness, | |
| "self_improvement": actions, | |
| "historical_vs_live": live, | |
| "truth_panel": trading_power.get("truth_panel", []), | |
| "policy": LEARNING_INTELLIGENCE_POLICY, | |
| } | |
| def latest_trading_game(db: Session) -> TradingGame | None: | |
| return TradeLedgerService().game(db) | |
| def game_trades(db: Session, game_id: int | None = None) -> list[TradingGameTrade]: | |
| if game_id: | |
| return list(db.scalars(select(TradingGameTrade).where(TradingGameTrade.game_id == game_id).order_by(TradingGameTrade.created_at)).all()) | |
| return list(db.scalars(select(TradingGameTrade).order_by(TradingGameTrade.created_at)).all()) | |
| def select_by_game_cycle(game_id: int | None): | |
| from app.models import TradingCapitalCycle | |
| query = select(TradingCapitalCycle) | |
| if game_id is not None: | |
| query = query.where(TradingCapitalCycle.game_id == game_id) | |
| return query | |
| def trading_power_components(rows: list[TradingGameTrade], metrics: dict, live_metrics: dict, cycle_payload: dict, reality: dict, benchmark_rows: list[dict], progress: dict) -> dict: | |
| benchmark_excess_values = [safe_float(item.get("excess_return")) for item in benchmark_rows if item.get("result_label") != "insufficient_sample" and item.get("excess_return") is not None] | |
| avg_excess = mean(benchmark_excess_values) if benchmark_excess_values else safe_float(metrics.get("benchmark_excess")) | |
| setup_count = len({row.setup_type for row in rows if row.setup_type}) | |
| regime_count = len({row.market_regime_at_entry for row in rows if row.market_regime_at_entry}) | |
| ticker_count = len({row.ticker for row in rows if row.ticker}) | |
| sample = int(metrics.get("trades_count") or 0) | |
| live_sample = int(live_metrics.get("trades_count") or 0) | |
| return { | |
| "benchmark_relative_score": round(clamp(50 + avg_excess * 4), 2), | |
| "expectancy_score": round(clamp(50 + safe_float(metrics.get("expectancy_r")) * 25), 2), | |
| "drawdown_control_score": round(clamp(100 + safe_float(metrics.get("max_drawdown")) * 4), 2) if metrics.get("max_drawdown") is not None else 35.0, | |
| "win_loss_quality_score": round(clamp(50 + (safe_float(metrics.get("win_rate")) - safe_float(metrics.get("loss_rate"))) * 60), 2), | |
| "missed_entry_penalty": round(clamp(safe_float(metrics.get("missed_entry_rate")) * 100), 2), | |
| "risk_management_score": round(clamp(mean(compact_numbers([metrics.get("sizing_quality_score"), metrics.get("risk_reward_quality_score"), reality.get("realism_score")])) if compact_numbers([metrics.get("sizing_quality_score"), metrics.get("risk_reward_quality_score"), reality.get("realism_score")]) else 35), 2), | |
| "capital_cycle_score": round(clamp(40 + safe_float(cycle_payload.get("target_hit_rate")) * 35 + safe_float(cycle_payload.get("survival_rate")) * 25), 2), | |
| "live_forward_validation_score": round(clamp(10 + min(50, live_sample) * 1.2 + safe_float(live_metrics.get("expectancy_r")) * 15), 2), | |
| "regime_robustness_score": round(clamp(20 + min(6, regime_count) * 10 + min(6, setup_count) * 4), 2), | |
| "setup_diversity_score": round(clamp(15 + min(12, setup_count) * 5 + min(20, ticker_count) * 1.2), 2), | |
| "statistical_confidence_score": round(clamp(10 + min(250, sample) * 0.22 + min(80, live_sample) * 0.35 + min(6, regime_count) * 4), 2), | |
| "reproducibility_score": round(clamp(safe_float(metrics.get("reproducibility_score"), 35)), 2), | |
| "decision_quality_score": round(clamp(mean(compact_numbers([metrics.get("entry_timing_score"), metrics.get("exit_timing_score"), metrics.get("trade_quality_score")])) if compact_numbers([metrics.get("entry_timing_score"), metrics.get("exit_timing_score"), metrics.get("trade_quality_score")]) else 35), 2), | |
| "learning_velocity_score": round(clamp(safe_float(progress.get("intelligence_growth_score"), metrics.get("intelligence_growth_score") or 0)), 2), | |
| } | |
| def trading_power_score(components: dict) -> float: | |
| weights = { | |
| "benchmark_relative_score": 0.13, | |
| "expectancy_score": 0.11, | |
| "drawdown_control_score": 0.08, | |
| "win_loss_quality_score": 0.08, | |
| "risk_management_score": 0.10, | |
| "capital_cycle_score": 0.07, | |
| "live_forward_validation_score": 0.12, | |
| "regime_robustness_score": 0.07, | |
| "setup_diversity_score": 0.05, | |
| "statistical_confidence_score": 0.10, | |
| "reproducibility_score": 0.05, | |
| "decision_quality_score": 0.08, | |
| "learning_velocity_score": 0.06, | |
| } | |
| raw = sum(safe_float(components.get(key)) * weight for key, weight in weights.items()) | |
| penalty = safe_float(components.get("missed_entry_penalty")) * 0.08 | |
| return clamp(raw - penalty) | |
| def classify_trading_power_score(score: float) -> str: | |
| if score <= 20: | |
| return "Not usable" | |
| if score <= 40: | |
| return "Weak / experimental" | |
| if score <= 60: | |
| return "Learning but not reliable" | |
| if score <= 75: | |
| return "Promising research system" | |
| if score <= 85: | |
| return "Strong paper-trading evidence" | |
| if score <= 95: | |
| return "Advanced alpha research candidate" | |
| return "Exceptional, requires external validation" | |
| def statistical_confidence_label(sample_size: int | None, live_sample_size: int | None = 0, context: dict | None = None) -> str: | |
| sample = int(sample_size or 0) | |
| live_sample = int(live_sample_size or 0) | |
| regimes = int((context or {}).get("regimes") or 0) | |
| tickers = int((context or {}).get("tickers") or 0) | |
| if sample < 30: | |
| return "very low evidence" | |
| if sample < 100 or tickers < 8: | |
| return "low evidence" | |
| if sample < 250 or regimes < 3 or live_sample < 10: | |
| return "medium evidence" | |
| if sample < 750 or live_sample < 30: | |
| return "strong evidence" | |
| return "high confidence" | |
| def benchmark_result_label(excess_return: float | None, sample_size: int) -> str: | |
| if sample_size < 30 or excess_return is None: | |
| return "insufficient_sample" | |
| if sample_size < 100 and abs(excess_return) < 2: | |
| return "inconclusive" | |
| if excess_return > 1: | |
| return "outperforming" | |
| if excess_return < -1: | |
| return "underperforming" | |
| return "similar" | |
| def truth_panel_from_payload(score: float, classification: str, warnings: list[str], benchmarks: list[dict], metrics: dict, live_metrics: dict) -> list[str]: | |
| rows = [f"BLUM Trading Power Score: {score:.1f}/100 ({classification})."] | |
| spy = next((item for item in benchmarks if item.get("benchmark_name") == "SPY"), None) | |
| qqq = next((item for item in benchmarks if item.get("benchmark_name") == "QQQ"), None) | |
| for item, label in [(spy, "SPY"), (qqq, "QQQ")]: | |
| if not item: | |
| continue | |
| result = item.get("result_label") | |
| excess = item.get("excess_return") | |
| if result == "underperforming": | |
| rows.append(f"BLUM is underperforming {label} on the current evidence ({round_or_none(excess)}% excess).") | |
| elif result == "outperforming": | |
| rows.append(f"BLUM is outperforming {label}, but this is still evidence-bound ({round_or_none(excess)}% excess).") | |
| else: | |
| rows.append(f"BLUM vs {label}: {result}; no strong conclusion.") | |
| if int(live_metrics.get("trades_count") or 0) < 30: | |
| rows.append("Live paper evidence is not mature yet; historical results are weaker evidence than forward paper trades.") | |
| if safe_float(metrics.get("missed_entry_rate")) > 0.25: | |
| rows.append("Missed entries remain a major weakness and must penalize actionability.") | |
| rows.extend(warnings[:4]) | |
| return dedupe_strings(rows) | |
| def trading_power_warnings(metrics: dict, live_metrics: dict, cycles: dict, reality: dict, benchmarks: list[dict], rows: list[TradingGameTrade]) -> list[str]: | |
| warnings = [] | |
| sample = int(metrics.get("trades_count") or 0) | |
| if sample < 100: | |
| warnings.append("Sample is still too small for a robust alpha claim.") | |
| if int(live_metrics.get("trades_count") or 0) < 30: | |
| warnings.append("Live forward paper sample is too small; historical simulation can overstate edge.") | |
| if safe_float(metrics.get("missed_entry_rate")) > 0.25: | |
| warnings.append("Missed-entry rate is high; BLUM may identify ideas but fail timing.") | |
| if any(item.get("result_label") == "underperforming" for item in benchmarks if item.get("benchmark_name") in {"SPY", "QQQ", "VTI"}): | |
| warnings.append("At least one major benchmark is currently beating BLUM on comparable evidence.") | |
| if (reality.get("warnings") or []): | |
| warnings.extend(str(item).replace("_", " ") for item in reality.get("warnings", [])[:3]) | |
| if len({row.market_regime_at_entry for row in rows if row.market_regime_at_entry}) < 3 and sample > 0: | |
| warnings.append("Regime coverage is thin; robustness across market conditions is not proven.") | |
| return dedupe_strings(warnings) | |
| def trading_power_explanation(score: float, classification: str, components: dict, warnings: list[str], metrics: dict, benchmarks: list[dict]) -> str: | |
| strongest = sorted(components.items(), key=lambda item: safe_float(item[1]), reverse=True)[:2] | |
| weakest = sorted((item for item in components.items() if item[0] != "missed_entry_penalty"), key=lambda item: safe_float(item[1]))[:2] | |
| benchmark_state = Counter(item.get("result_label") for item in benchmarks) | |
| return ( | |
| f"Score {score:.1f}/100 ({classification}). Strongest components: " | |
| f"{', '.join(f'{name}={value}' for name, value in strongest)}. Weakest components: " | |
| f"{', '.join(f'{name}={value}' for name, value in weakest)}. Benchmark states: {dict(benchmark_state)}. " | |
| f"Trades: {metrics.get('trades_count', 0)}. Main warning: {warnings[0] if warnings else 'no critical warning'}." | |
| ) | |
| def benchmark_returns_for_rows(db: Session, rows: list[TradingGameTrade], benchmark: str) -> list[float]: | |
| direct = [safe_float(row.benchmark_return_same_period) for row in rows if (row.benchmark_ticker or "").upper() == benchmark.upper() and row.benchmark_return_same_period is not None] | |
| if direct: | |
| return direct | |
| period_return = price_period_return(db, benchmark, rows) | |
| if period_return is not None: | |
| return [period_return for _ in rows] | |
| fallback = [safe_float(row.benchmark_return_same_period) for row in rows if row.benchmark_return_same_period is not None] | |
| return fallback | |
| def price_period_return(db: Session, ticker: str, rows: list[TradingGameTrade]) -> float | None: | |
| start = parse_date_value(first_date(rows)) | |
| end = parse_date_value(last_date(rows)) | |
| if not start or not end: | |
| return None | |
| asset = db.scalar(select(Asset).where(Asset.ticker == ticker).limit(1)) | |
| if not asset: | |
| return None | |
| first = db.scalar(select(PriceHistory).where(PriceHistory.asset_id == asset.id, PriceHistory.date >= start).order_by(PriceHistory.date).limit(1)) | |
| last = db.scalar(select(PriceHistory).where(PriceHistory.asset_id == asset.id, PriceHistory.date <= end).order_by(desc(PriceHistory.date)).limit(1)) | |
| if not first or not last or not first.close: | |
| return None | |
| return (last.close / first.close - 1) * 100 | |
| def baseline_return(name: str, rows: list[TradingGameTrade]) -> float | None: | |
| returns = trade_returns(rows) | |
| if not returns: | |
| return None | |
| if name == "cash_no_trade_baseline": | |
| return 0.0 | |
| if name == "random_asset_selection_proxy": | |
| benchmarks = [safe_float(row.benchmark_return_same_period) for row in rows if row.benchmark_return_same_period is not None] | |
| return median(benchmarks) if benchmarks else 0.0 | |
| if name == "random_entry_exit_proxy": | |
| return median(returns) * 0.65 | |
| if name == "momentum_baseline_proxy": | |
| momentum_rows = [row for row in rows if "momentum" in (row.setup_type or "")] | |
| values = trade_returns(momentum_rows) | |
| return mean(values) if values else median(returns) | |
| if name == "moving_average_crossover_proxy": | |
| trend_rows = [row for row in rows if "trend" in (row.setup_type or "") or "pullback" in (row.setup_type or "")] | |
| values = trade_returns(trend_rows) | |
| return mean(values) if values else median(returns) | |
| if name == "sector_rotation_proxy": | |
| sector_rows = [row for row in rows if "sector" in (row.setup_type or "") or "rotation" in (row.setup_type or "")] | |
| values = trade_returns(sector_rows) | |
| return mean(values) if values else median(returns) | |
| return None | |
| def trade_returns(rows: list[TradingGameTrade]) -> list[float]: | |
| output = [] | |
| for row in rows: | |
| value = row.pnl_percent | |
| if value is None: | |
| value = safe_float(row.net_pnl_eur if row.net_pnl_eur is not None else row.realized_pl) / max(0.01, safe_float(row.capital_before)) * 100 | |
| output.append(safe_float(value)) | |
| return output | |
| def weakness_from_metric(dimension: str, metric: dict) -> dict: | |
| sample = int(metric.get("trades_count") or 0) | |
| missed = safe_float(metric.get("missed_entry_rate")) | |
| stop = safe_float(metric.get("stop_hit_rate")) | |
| expectancy = safe_float(metric.get("expectancy_r")) | |
| benchmark = safe_float(metric.get("benchmark_excess")) | |
| quality = safe_float(metric.get("trade_quality_score"), 45) | |
| weakness = clamp((missed * 28) + (stop * 22) + max(0, -expectancy) * 22 + max(0, -benchmark) * 2 + max(0, 55 - quality) * 0.45 + (25 if sample < settings.self_improvement_min_sample_size else 0)) | |
| strength = clamp(100 - weakness + max(0, expectancy) * 12 + max(0, benchmark) * 0.8) | |
| problem, action, module = weakness_problem_action(dimension, metric, weakness) | |
| priority = "high" if weakness >= 70 or missed > 0.4 or stop > 0.65 or benchmark < -5 else "medium" if weakness >= 45 or missed > 0.25 or stop > 0.5 or benchmark < -1 else "low" | |
| return { | |
| "dimension": dimension, | |
| "entity": str(metric.get("scope_id") or "unknown"), | |
| "strength_score": round(strength, 2), | |
| "weakness_score": round(weakness, 2), | |
| "sample_size": sample, | |
| "evidence": metric, | |
| "main_problem": problem, | |
| "recommended_action": action, | |
| "priority": priority, | |
| "status": "open", | |
| "affected_module": module, | |
| } | |
| def weakness_problem_action(dimension: str, metric: dict, weakness: float) -> tuple[str, str, str]: | |
| entity = str(metric.get("scope_id") or "unknown").replace("_", " ") | |
| if safe_float(metric.get("missed_entry_rate")) > 0.25: | |
| return ( | |
| f"High missed-entry rate in {dimension}={entity}.", | |
| "Test pullback-retest entry logic against breakout-close entry logic and penalize late entries.", | |
| "EntryExitEngine", | |
| ) | |
| if safe_float(metric.get("stop_hit_rate")) > 0.55: | |
| return ( | |
| f"Stop-hit rate is elevated in {dimension}={entity}.", | |
| "Re-test stop placement, ATR buffers and hostile-regime no-trade filters.", | |
| "RiskEngine", | |
| ) | |
| if safe_float(metric.get("benchmark_excess")) < -1: | |
| return ( | |
| f"Underperforming benchmark in {dimension}={entity}.", | |
| "Compare against passive benchmark and simple momentum baseline before increasing confidence.", | |
| "BenchmarkComparisonService", | |
| ) | |
| if metric.get("trades_count", 0) < settings.self_improvement_min_sample_size: | |
| return ( | |
| f"Insufficient sample in {dimension}={entity}.", | |
| "Increase adaptive Learning Loop sampling before changing weights.", | |
| "LearningLoopService", | |
| ) | |
| if weakness >= 45: | |
| return ( | |
| f"Mixed decision quality in {dimension}={entity}.", | |
| "Study entry, exit and no-trade outcomes separately before changing Sniper Score.", | |
| "SniperScoreCalculator", | |
| ) | |
| return ( | |
| f"No critical weakness in {dimension}={entity}.", | |
| "Keep monitoring and avoid overfitting to a strong but narrow sample.", | |
| "MetaIntelligence", | |
| ) | |
| def self_improvement_action_from_weakness(weakness: dict) -> dict: | |
| metric = weakness.get("evidence") or {} | |
| sample = int(weakness.get("sample_size") or 0) | |
| priority = weakness.get("priority") or "medium" | |
| source_dimension = f"{weakness.get('dimension')}:{weakness.get('entity')}" | |
| return { | |
| "source_metric": "learning_strength_weakness_map", | |
| "source_dimension": source_dimension, | |
| "detected_problem": weakness.get("main_problem") or "Measured weakness without classified problem.", | |
| "recommended_action": weakness.get("recommended_action") or "Collect more samples before changing parameters.", | |
| "affected_module": weakness.get("affected_module") or "LearningLoopService", | |
| "priority": priority, | |
| "expected_impact": "Reduce false positives, missed entries or benchmark underperformance while preserving reversibility.", | |
| "status": "proposed", | |
| "before_metric": metric.get("intelligence_growth_score") or metric.get("expectancy_r"), | |
| "after_metric": None, | |
| "improvement_observed": None, | |
| "notes_json": { | |
| "sample_size": sample, | |
| "auto_apply_allowed": settings.self_improvement_auto_apply or (settings.self_improvement_auto_apply_low_risk and priority == "low"), | |
| "reversible": True, | |
| "source_code_self_modification": False, | |
| "requires_human_review": priority != "low" or not settings.self_improvement_auto_apply, | |
| }, | |
| } | |
| def latest_metric_for_action(db: Session, action: SelfImprovementAction) -> float | None: | |
| dimension, _, entity = (action.source_dimension or "").partition(":") | |
| rows = LearningWeaknessMapService().map(db, dimension if dimension else None, persist=False).get("rows", []) | |
| match = next((row for row in rows if row.get("entity") == entity), None) | |
| if not match: | |
| return None | |
| evidence = match.get("evidence") or {} | |
| return evidence.get("intelligence_growth_score") or evidence.get("expectancy_r") | |
| def progress_snapshot_row(payload: dict, trend: str, growth: float, window_size: int | None) -> LearningProgressSnapshot: | |
| return LearningProgressSnapshot( | |
| window_type=payload.get("window_type") or "all", | |
| window_size=window_size, | |
| trades_count=payload.get("trades_count", 0), | |
| win_rate=payload.get("win_rate"), | |
| missed_entry_rate=payload.get("missed_entry_rate"), | |
| loss_rate=payload.get("loss_rate"), | |
| target_hit_rate=payload.get("target_hit_rate"), | |
| stop_hit_rate=payload.get("stop_hit_rate"), | |
| expectancy_r=payload.get("expectancy_r"), | |
| benchmark_excess=payload.get("benchmark_excess"), | |
| max_drawdown=payload.get("max_drawdown"), | |
| trade_quality_avg=payload.get("trade_quality_score"), | |
| confidence_calibration_error=None, | |
| repeated_mistake_rate=None, | |
| intelligence_growth_score=round(growth, 2), | |
| trend_label=trend, | |
| explanation=progress_explanation(trend, growth, [payload], payload), | |
| ) | |
| def progress_trend_label(windows: list[dict]) -> str: | |
| latest = next((item for item in windows if item.get("window_size") == 30), None) | |
| prior = next((item for item in windows if item.get("window_size") == 100), None) | |
| if not latest or not prior or latest.get("trades_count", 0) < 15 or prior.get("trades_count", 0) < 30: | |
| return "inconclusive" | |
| delta = safe_float(latest.get("expectancy_r")) - safe_float(prior.get("expectancy_r")) | |
| if delta > 0.08: | |
| return "improving" | |
| if delta < -0.08: | |
| return "deteriorating" | |
| return "stable" | |
| def intelligence_growth_score(windows: list[dict], current: dict) -> float: | |
| base = safe_float(current.get("intelligence_growth_score")) | |
| latest = next((item for item in windows if item.get("window_size") == 30), None) | |
| prior = next((item for item in windows if item.get("window_size") == 100), None) | |
| if latest and prior: | |
| delta = safe_float(latest.get("intelligence_growth_score")) - safe_float(prior.get("intelligence_growth_score")) | |
| base += delta * 0.35 | |
| return clamp(base) | |
| def progress_explanation(trend: str, score: float, windows: list[dict], current: dict) -> str: | |
| return ( | |
| f"Learning trend is {trend}; Intelligence Growth Score is {score:.1f}/100. " | |
| f"Current sample: {current.get('trades_count', 0)} trades/actions. " | |
| "The score is cautious when live evidence, sample size or benchmark confirmation are incomplete." | |
| ) | |
| def benchmark_explanation(benchmark: str, benchmark_type: str, sample: int, blum_return: float | None, benchmark_return: float | None, excess: float | None, label: str, benchmark_returns: list[float]) -> str: | |
| source = "same-period benchmark rows" if benchmark_returns else "price-history period proxy or unavailable fallback" | |
| return ( | |
| f"BLUM vs {benchmark} ({benchmark_type}) uses {source}. Sample={sample}. " | |
| f"BLUM trade-weighted return={round_or_none(blum_return)}%, benchmark={round_or_none(benchmark_return)}%, " | |
| f"excess={round_or_none(excess)}%. Result={label}." | |
| ) | |
| def baseline_explanation(name: str, sample: int, baseline: float | None, excess: float | None, label: str) -> str: | |
| return ( | |
| f"{name} is an internal baseline proxy built from the same decision ledger, not an external execution proof. " | |
| f"Sample={sample}; baseline={round_or_none(baseline)}%; excess={round_or_none(excess)}%; result={label}." | |
| ) | |
| def downside_volatility(values: list[float]) -> float | None: | |
| downside = [value for value in values if value < 0] | |
| return pstdev(downside) if len(downside) > 1 else None | |
| def ratio_or_none(numerator: float | None, denominator: float | None) -> float | None: | |
| if numerator is None or denominator is None or abs(denominator) < 0.0001: | |
| return None | |
| return round(numerator / denominator, 4) | |
| def hit_rate_vs_benchmark(returns: list[float], benchmark_returns: list[float]) -> float | None: | |
| if not returns or not benchmark_returns: | |
| return None | |
| paired = list(zip(returns, benchmark_returns)) | |
| return sum(1 for blum, bench in paired if blum > bench) / max(1, len(paired)) | |
| def round_or_none(value: float | None, digits: int = 4) -> float | None: | |
| if value is None: | |
| return None | |
| return round(float(value), digits) | |
| def compact_numbers(values: list[object]) -> list[float]: | |
| return [safe_float(value) for value in values if value is not None] | |
| def first_date(rows: list[TradingGameTrade]) -> str | None: | |
| dates = [row.entry_date for row in rows if row.entry_date] | |
| return min(dates).isoformat() if dates else None | |
| def last_date(rows: list[TradingGameTrade]) -> str | None: | |
| dates = [row.exit_date or row.entry_date for row in rows if row.exit_date or row.entry_date] | |
| return max(dates).isoformat() if dates else None | |
| def parse_date_value(value: str | date | datetime | None) -> date | None: | |
| if value is None: | |
| return None | |
| if isinstance(value, datetime): | |
| return value.date() | |
| if isinstance(value, date): | |
| return value | |
| try: | |
| return datetime.fromisoformat(str(value)).date() | |
| except ValueError: | |
| return None | |
| def dedupe_strings(rows: list[str]) -> list[str]: | |
| output = [] | |
| seen = set() | |
| for row in rows: | |
| text = str(row).strip() | |
| key = text.lower() | |
| if not text or key in seen: | |
| continue | |
| seen.add(key) | |
| output.append(text) | |
| return output | |
| def dedupe_actions(rows: list[dict]) -> list[dict]: | |
| output = [] | |
| seen = set() | |
| for row in rows: | |
| key = (row.get("source_dimension"), row.get("detected_problem"), row.get("affected_module")) | |
| if key in seen: | |
| continue | |
| seen.add(key) | |
| output.append(row) | |
| return output | |
| def priority_rank(priority: str) -> int: | |
| return {"low": 1, "medium": 2, "high": 3}.get(priority, 0) | |
| def serialize_action(row: SelfImprovementAction) -> dict: | |
| return { | |
| "id": row.id, | |
| "created_at": row.created_at.isoformat() if row.created_at else None, | |
| "source_metric": row.source_metric, | |
| "source_dimension": row.source_dimension, | |
| "detected_problem": row.detected_problem, | |
| "recommended_action": row.recommended_action, | |
| "affected_module": row.affected_module, | |
| "priority": row.priority, | |
| "expected_impact": row.expected_impact, | |
| "status": row.status, | |
| "applied_at": row.applied_at.isoformat() if row.applied_at else None, | |
| "before_metric": row.before_metric, | |
| "after_metric": row.after_metric, | |
| "improvement_observed": row.improvement_observed, | |
| "notes_json": row.notes_json, | |
| } | |