diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-10-ForwardInduction-SPE.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-10-ForwardInduction-SPE.ipynb index e0fbc58833..52e9839246 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-10-ForwardInduction-SPE.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-10-ForwardInduction-SPE.ipynb @@ -2065,13 +2065,34 @@ "cell_type": "markdown", "id": "2277837f", "metadata": {}, - "source": "**Lien avec la formalisation Lean**\n\nL'equilibre de sous-jeux parfaits (SPE) raffine le concept d'equilibre de Nash en imposant la rationalite dans chaque sous-jeu. La structure `GameTree` et l'algorithme de minimax dans [`lean_game_defs/Combinatorial.lean`](lean_game_defs/Combinatorial.lean) formalisent l'induction a rebours pour les jeux a information parfaite -- le cas particulier zero-somme du backward induction que SPE generalise. Les concepts de forward induction et de perfection en main tremblante restent a formaliser." + "source": [ + "**Lien avec la formalisation Lean**\n", + "\n", + "L'equilibre de sous-jeux parfaits (SPE) raffine le concept d'equilibre de Nash en imposant la rationalite dans chaque sous-jeu. La structure `GameTree` et l'algorithme de minimax dans [`lean_game_defs/Combinatorial.lean`](lean_game_defs/Combinatorial.lean) formalisent l'induction a rebours pour les jeux a information parfaite -- le cas particulier zero-somme du backward induction que SPE generalise. Les concepts de forward induction et de perfection en main tremblante restent a formaliser." + ] }, { "cell_type": "markdown", "id": "cfa25ede", "metadata": {}, - "source": "## Resume et perspectives\n\nCe notebook a constitue une étude approfondie des raffinements d'equilibre pour les jeux sous forme extensive, en progressant dans une hiérarchie de pouvoir discriminant croissant. L'equilibre parfait de sous-jeux (SPE) a elimine les menaces non credibles en exigeant un Nash dans chaque sous-jeu, comme l'a illustre le jeu d'entree ou la menace de combat n'est pas soutenable. La perfection en mains tremblantes a renforce cette sélection en eliminant les equilibres reposant sur des stratégies faiblement dominees, ne conservant que les profils robustes aux petites erreurs. L'induction avant a introduit un raisonnement d'une nature différente : plutot que de partir de la fin du jeu, elle exploite les actions observees en debut de partie pour inferer les intentions des joueurs, selectionnant ainsi l'equilibre efficient dans la Chasse au Cerf avec option exterieure.\n\nLe jeu du \"Burn Money\" a synthetise ces concepts de maniere frappante : la simple possibilite de bruler de l'argent modifie l'equilibre même si personne ne brule effectivement, un phenomene qui rappelle que les options non utilisees peuvent etre aussi informatives que les actions realisees. La hiérarchie des raffinements -- Nash, SPE, trembling-hand, forward induction -- forme un outil d'analyse progressif dont le choix depend du contexte et de la richesse informationnelle du jeu etudie.\n\nL'étape suivante introduit les jeux bayesiens et l'information incomplete, ou les joueurs ne connaissent pas les types de leurs adversaires : [GameTheory-11-BayesianGames](GameTheory-11-BayesianGames.ipynb).\n\n\n\n### References academiques\n\n- Selten, R. (1965). Spieltheoretische Behandlung eines Oligopolmodells mit Nachfragetragheit. Jahrbucher fur Nationalokonomie und Statistik 183:301-324.\n- Selten, R. (1975). Reexamination of the Perfectness Concept for Equilibrium Points in Extensive Games. International Journal of Game Theory 4(1):25-55.\n- Kohlberg, E. & Mertens, J.-F. (1986). On the Strategic Stability of Equilibria. Journal of Economic Theory 41(2):353-379.\n\n" + "source": [ + "## Resume et perspectives\n", + "\n", + "Ce notebook a constitue une étude approfondie des raffinements d'equilibre pour les jeux sous forme extensive, en progressant dans une hiérarchie de pouvoir discriminant croissant. L'equilibre parfait de sous-jeux (SPE) a elimine les menaces non credibles en exigeant un Nash dans chaque sous-jeu, comme l'a illustre le jeu d'entree ou la menace de combat n'est pas soutenable. La perfection en mains tremblantes a renforce cette sélection en eliminant les equilibres reposant sur des stratégies faiblement dominees, ne conservant que les profils robustes aux petites erreurs. L'induction avant a introduit un raisonnement d'une nature différente : plutot que de partir de la fin du jeu, elle exploite les actions observees en debut de partie pour inferer les intentions des joueurs, selectionnant ainsi l'equilibre efficient dans la Chasse au Cerf avec option exterieure.\n", + "\n", + "Le jeu du \"Burn Money\" a synthetise ces concepts de maniere frappante : la simple possibilite de bruler de l'argent modifie l'equilibre même si personne ne brule effectivement, un phenomene qui rappelle que les options non utilisees peuvent etre aussi informatives que les actions realisees. La hiérarchie des raffinements -- Nash, SPE, trembling-hand, forward induction -- forme un outil d'analyse progressif dont le choix depend du contexte et de la richesse informationnelle du jeu etudie.\n", + "\n", + "L'étape suivante introduit les jeux bayesiens et l'information incomplete, ou les joueurs ne connaissent pas les types de leurs adversaires : [GameTheory-11-BayesianGames](GameTheory-11-BayesianGames.ipynb).\n", + "\n", + "\n", + "\n", + "### References academiques\n", + "\n", + "- Selten, R. (1965). Spieltheoretische Behandlung eines Oligopolmodells mit Nachfragetragheit. Jahrbucher fur Nationalokonomie und Statistik 183:301-324.\n", + "- Selten, R. (1975). Reexamination of the Perfectness Concept for Equilibrium Points in Extensive Games. International Journal of Game Theory 4(1):25-55.\n", + "- Kohlberg, E. & Mertens, J.-F. (1986). On the Strategic Stability of Equilibria. Journal of Economic Theory 41(2):353-379.\n", + "\n" + ] } ], "metadata": { diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/Research-Executor/runner.ipynb b/MyIA.AI.Notebooks/QuantConnect/projects/Research-Executor/runner.ipynb index 95ed8fa321..dc140996f7 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/Research-Executor/runner.ipynb +++ b/MyIA.AI.Notebooks/QuantConnect/projects/Research-Executor/runner.ipynb @@ -12,7 +12,80 @@ } }, "outputs": [], - "source": "import json\nimport sys\nimport os\nfrom io import StringIO\nimport traceback\n\ncwd = os.getcwd()\nfiles = sorted(os.listdir('.'))\nprint(f'Working directory: {cwd}')\nprint(f'Files: {files}')\n\nnotebooks = [\n 'research_asset_class_momentum.ipynb',\n 'research_volatility_regime_ml.ipynb',\n 'research_defensive_etf_rotation.ipynb',\n 'research_commodity_term_structure.ipynb',\n 'research_piotroski_fscore.ipynb',\n 'research_long_short_harvest.ipynb',\n 'research_macro_factor_rotation.ipynb',\n 'research_puppies_of_dow.ipynb',\n]\n\nfor nb_name in notebooks:\n print(f'\\n{\"=\" * 60}')\n print(f'Processing: {nb_name}')\n try:\n with open(nb_name, 'r') as f:\n nb = json.load(f)\n except FileNotFoundError:\n print(f'SKIP: {nb_name} not found')\n continue\n exec_count = 0\n success = 0\n errors = 0\n for cell in nb['cells']:\n if cell['cell_type'] != 'code':\n continue\n exec_count += 1\n code = ''.join(cell['source'])\n old_stdout, old_stderr = sys.stdout, sys.stderr\n sys.stdout, sys.stderr = StringIO(), StringIO()\n try:\n exec(code, globals())\n cell['execution_count'] = exec_count\n out = sys.stdout.getvalue()\n err = sys.stderr.getvalue()\n outputs = []\n if out:\n lines = out.splitlines(True)\n if lines and not lines[-1].endswith('\\n'):\n lines[-1] += '\\n'\n outputs.append({'output_type': 'stream', 'name': 'stdout', 'text': lines})\n if err:\n outputs.append({'output_type': 'stream', 'name': 'stderr', 'text': err.splitlines(True)})\n cell['outputs'] = outputs\n success += 1\n except Exception as e:\n cell['execution_count'] = exec_count\n cell['outputs'] = [{\n 'output_type': 'error',\n 'ename': type(e).__name__,\n 'evalue': str(e),\n 'traceback': [traceback.format_exc()]\n }]\n errors += 1\n finally:\n sys.stdout, sys.stderr = old_stdout, old_stderr\n out_name = nb_name.replace('.ipynb', '_executed.ipynb')\n with open(out_name, 'w') as f:\n json.dump(nb, f, indent=1)\n print(f' {success} OK, {errors} ERR -> {out_name}')\nprint('\\n\\nALL DONE')" + "source": [ + "import json\n", + "import sys\n", + "import os\n", + "from io import StringIO\n", + "import traceback\n", + "\n", + "cwd = os.getcwd()\n", + "files = sorted(os.listdir('.'))\n", + "print(f'Working directory: {cwd}')\n", + "print(f'Files: {files}')\n", + "\n", + "notebooks = [\n", + " 'research_asset_class_momentum.ipynb',\n", + " 'research_volatility_regime_ml.ipynb',\n", + " 'research_defensive_etf_rotation.ipynb',\n", + " 'research_commodity_term_structure.ipynb',\n", + " 'research_piotroski_fscore.ipynb',\n", + " 'research_long_short_harvest.ipynb',\n", + " 'research_macro_factor_rotation.ipynb',\n", + " 'research_puppies_of_dow.ipynb',\n", + "]\n", + "\n", + "for nb_name in notebooks:\n", + " print(f'\\n{\"=\" * 60}')\n", + " print(f'Processing: {nb_name}')\n", + " try:\n", + " with open(nb_name, 'r') as f:\n", + " nb = json.load(f)\n", + " except FileNotFoundError:\n", + " print(f'SKIP: {nb_name} not found')\n", + " continue\n", + " exec_count = 0\n", + " success = 0\n", + " errors = 0\n", + " for cell in nb['cells']:\n", + " if cell['cell_type'] != 'code':\n", + " continue\n", + " exec_count += 1\n", + " code = ''.join(cell['source'])\n", + " old_stdout, old_stderr = sys.stdout, sys.stderr\n", + " sys.stdout, sys.stderr = StringIO(), StringIO()\n", + " try:\n", + " exec(code, globals())\n", + " cell['execution_count'] = exec_count\n", + " out = sys.stdout.getvalue()\n", + " err = sys.stderr.getvalue()\n", + " outputs = []\n", + " if out:\n", + " lines = out.splitlines(True)\n", + " if lines and not lines[-1].endswith('\\n'):\n", + " lines[-1] += '\\n'\n", + " outputs.append({'output_type': 'stream', 'name': 'stdout', 'text': lines})\n", + " if err:\n", + " outputs.append({'output_type': 'stream', 'name': 'stderr', 'text': err.splitlines(True)})\n", + " cell['outputs'] = outputs\n", + " success += 1\n", + " except Exception as e:\n", + " cell['execution_count'] = exec_count\n", + " cell['outputs'] = [{\n", + " 'output_type': 'error',\n", + " 'ename': type(e).__name__,\n", + " 'evalue': str(e),\n", + " 'traceback': [traceback.format_exc()]\n", + " }]\n", + " errors += 1\n", + " finally:\n", + " sys.stdout, sys.stderr = old_stdout, old_stderr\n", + " out_name = nb_name.replace('.ipynb', '_executed.ipynb')\n", + " with open(out_name, 'w') as f:\n", + " json.dump(nb, f, indent=1)\n", + " print(f' {success} OK, {errors} ERR -> {out_name}')\n", + "print('\\n\\nALL DONE')" + ] } ], "metadata": { diff --git a/MyIA.AI.Notebooks/QuantConnect/research/research_m11ef_ensemble.ipynb b/MyIA.AI.Notebooks/QuantConnect/research/research_m11ef_ensemble.ipynb index b39514d846..f1c3a2ccd8 100644 --- a/MyIA.AI.Notebooks/QuantConnect/research/research_m11ef_ensemble.ipynb +++ b/MyIA.AI.Notebooks/QuantConnect/research/research_m11ef_ensemble.ipynb @@ -121,7 +121,67 @@ ] } ], - "source": "# Section 3: Kelly threshold-band position sizing\ndef threshold_band_signal(close_prices, window=60):\n \"\"\"Generate signal: long if price > rolling mean, short otherwise.\"\"\"\n prices = pd.Series(close_prices)\n rolling_mean = prices.rolling(window).mean()\n signal = np.where(prices.values > rolling_mean.values, 1.0, 0.0)\n return signal\n\ndef kelly_sleeve(close_prices, cap=0.6, window=60, fee_bps=50):\n \"\"\"Single Kelly sleeve with threshold-band signal.\"\"\"\n prices = pd.Series(close_prices)\n returns = prices.pct_change().dropna().values\n\n # Signal aligned to returns length (returns starts at index 1)\n signal_full = threshold_band_signal(close_prices, window)\n signal = signal_full[1:][:len(returns)] # skip first element (no return), trim to match\n\n # Rolling vol for Kelly sizing\n rolling_vol = pd.Series(returns).rolling(window).std().values * np.sqrt(252)\n rolling_vol = np.where(rolling_vol < 0.01, 0.2, rolling_vol) # floor\n\n # Kelly weight: cap * (signal / vol)\n kelly_w = np.minimum(cap / rolling_vol, cap) * signal\n\n # Net returns\n weighted_returns = kelly_w * returns\n\n # Fee deduction\n fee = fee_bps / 10000\n trades = np.abs(np.diff(np.concatenate([[0], kelly_w])))\n fee_costs = np.concatenate([[0], trades * fee])[:len(weighted_returns)]\n net_returns = weighted_returns - fee_costs\n\n sharpe = np.mean(net_returns) / (np.std(net_returns) + 1e-10) * np.sqrt(252)\n return sharpe, net_returns\n\ndef vol_targeting_sleeve(close_prices, target_vol=0.15, window=60, fee_bps=50):\n \"\"\"Vol-targeting sleeve: scale position to maintain target volatility.\"\"\"\n prices = pd.Series(close_prices)\n returns = prices.pct_change().dropna().values\n\n rolling_vol = pd.Series(returns).rolling(window).std().values * np.sqrt(252)\n rolling_vol = np.where(rolling_vol < 0.01, 0.2, rolling_vol)\n\n # Scale to target vol\n scale = target_vol / rolling_vol\n scale = np.minimum(scale, 2.0) # cap leverage\n\n weighted_returns = scale * returns\n\n fee = fee_bps / 10000\n trades = np.abs(np.diff(np.concatenate([[0], scale])))\n fee_costs = np.concatenate([[0], trades * fee])[:len(weighted_returns)]\n net_returns = weighted_returns - fee_costs\n\n sharpe = np.mean(net_returns) / (np.std(net_returns) + 1e-10) * np.sqrt(252)\n return sharpe, net_returns\n\nprint(\"Sleeve functions defined\")", + "source": [ + "# Section 3: Kelly threshold-band position sizing\n", + "def threshold_band_signal(close_prices, window=60):\n", + " \"\"\"Generate signal: long if price > rolling mean, short otherwise.\"\"\"\n", + " prices = pd.Series(close_prices)\n", + " rolling_mean = prices.rolling(window).mean()\n", + " signal = np.where(prices.values > rolling_mean.values, 1.0, 0.0)\n", + " return signal\n", + "\n", + "def kelly_sleeve(close_prices, cap=0.6, window=60, fee_bps=50):\n", + " \"\"\"Single Kelly sleeve with threshold-band signal.\"\"\"\n", + " prices = pd.Series(close_prices)\n", + " returns = prices.pct_change().dropna().values\n", + "\n", + " # Signal aligned to returns length (returns starts at index 1)\n", + " signal_full = threshold_band_signal(close_prices, window)\n", + " signal = signal_full[1:][:len(returns)] # skip first element (no return), trim to match\n", + "\n", + " # Rolling vol for Kelly sizing\n", + " rolling_vol = pd.Series(returns).rolling(window).std().values * np.sqrt(252)\n", + " rolling_vol = np.where(rolling_vol < 0.01, 0.2, rolling_vol) # floor\n", + "\n", + " # Kelly weight: cap * (signal / vol)\n", + " kelly_w = np.minimum(cap / rolling_vol, cap) * signal\n", + "\n", + " # Net returns\n", + " weighted_returns = kelly_w * returns\n", + "\n", + " # Fee deduction\n", + " fee = fee_bps / 10000\n", + " trades = np.abs(np.diff(np.concatenate([[0], kelly_w])))\n", + " fee_costs = np.concatenate([[0], trades * fee])[:len(weighted_returns)]\n", + " net_returns = weighted_returns - fee_costs\n", + "\n", + " sharpe = np.mean(net_returns) / (np.std(net_returns) + 1e-10) * np.sqrt(252)\n", + " return sharpe, net_returns\n", + "\n", + "def vol_targeting_sleeve(close_prices, target_vol=0.15, window=60, fee_bps=50):\n", + " \"\"\"Vol-targeting sleeve: scale position to maintain target volatility.\"\"\"\n", + " prices = pd.Series(close_prices)\n", + " returns = prices.pct_change().dropna().values\n", + "\n", + " rolling_vol = pd.Series(returns).rolling(window).std().values * np.sqrt(252)\n", + " rolling_vol = np.where(rolling_vol < 0.01, 0.2, rolling_vol)\n", + "\n", + " # Scale to target vol\n", + " scale = target_vol / rolling_vol\n", + " scale = np.minimum(scale, 2.0) # cap leverage\n", + "\n", + " weighted_returns = scale * returns\n", + "\n", + " fee = fee_bps / 10000\n", + " trades = np.abs(np.diff(np.concatenate([[0], scale])))\n", + " fee_costs = np.concatenate([[0], trades * fee])[:len(weighted_returns)]\n", + " net_returns = weighted_returns - fee_costs\n", + "\n", + " sharpe = np.mean(net_returns) / (np.std(net_returns) + 1e-10) * np.sqrt(252)\n", + " return sharpe, net_returns\n", + "\n", + "print(\"Sleeve functions defined\")" + ], "id": "cell-a896b9f2" }, { @@ -162,7 +222,43 @@ ] } ], - "source": "# Section 4: Individual sleeve backtests\nfee_levels = [0, 5, 10, 25, 50, 100]\nsleeve_results = []\n\nfor symbol, hist in crypto_data.items():\n close = hist[\"close\"].values\n \n for fee in fee_levels:\n # K60\n sharpe_k60, rets_k60 = kelly_sleeve(close, cap=0.6, fee_bps=fee)\n # K30\n sharpe_k30, rets_k30 = kelly_sleeve(close, cap=0.3, fee_bps=fee)\n # VTH\n sharpe_vth, rets_vth = vol_targeting_sleeve(close, target_vol=0.15, fee_bps=fee)\n \n # Buy-and-hold baseline (round-trip fee: entry + exit)\n bh_returns = np.diff(np.log(close))\n bh_fee = 2 * fee / 10000 # round-trip: entry + exit\n bh_net = bh_returns - bh_fee\n sharpe_bh = np.mean(bh_net) / (np.std(bh_net) + 1e-10) * np.sqrt(252)\n \n sleeve_results.append({\n \"coin\": symbol,\n \"fee_bps\": fee,\n \"sharpe_k60\": sharpe_k60,\n \"sharpe_k30\": sharpe_k30,\n \"sharpe_vth\": sharpe_vth,\n \"sharpe_bh\": sharpe_bh,\n })\n \n print(f\"{symbol} @ {fee}bps: K60={sharpe_k60:.3f}, K30={sharpe_k30:.3f}, \"\n f\"VTH={sharpe_vth:.3f}, BH={sharpe_bh:.3f}\")\n\nsleeve_df = pd.DataFrame(sleeve_results)\nprint(f\"\\nTotal combos: {len(sleeve_df)}\")", + "source": [ + "# Section 4: Individual sleeve backtests\n", + "fee_levels = [0, 5, 10, 25, 50, 100]\n", + "sleeve_results = []\n", + "\n", + "for symbol, hist in crypto_data.items():\n", + " close = hist[\"close\"].values\n", + " \n", + " for fee in fee_levels:\n", + " # K60\n", + " sharpe_k60, rets_k60 = kelly_sleeve(close, cap=0.6, fee_bps=fee)\n", + " # K30\n", + " sharpe_k30, rets_k30 = kelly_sleeve(close, cap=0.3, fee_bps=fee)\n", + " # VTH\n", + " sharpe_vth, rets_vth = vol_targeting_sleeve(close, target_vol=0.15, fee_bps=fee)\n", + " \n", + " # Buy-and-hold baseline (round-trip fee: entry + exit)\n", + " bh_returns = np.diff(np.log(close))\n", + " bh_fee = 2 * fee / 10000 # round-trip: entry + exit\n", + " bh_net = bh_returns - bh_fee\n", + " sharpe_bh = np.mean(bh_net) / (np.std(bh_net) + 1e-10) * np.sqrt(252)\n", + " \n", + " sleeve_results.append({\n", + " \"coin\": symbol,\n", + " \"fee_bps\": fee,\n", + " \"sharpe_k60\": sharpe_k60,\n", + " \"sharpe_k30\": sharpe_k30,\n", + " \"sharpe_vth\": sharpe_vth,\n", + " \"sharpe_bh\": sharpe_bh,\n", + " })\n", + " \n", + " print(f\"{symbol} @ {fee}bps: K60={sharpe_k60:.3f}, K30={sharpe_k30:.3f}, \"\n", + " f\"VTH={sharpe_vth:.3f}, BH={sharpe_bh:.3f}\")\n", + "\n", + "sleeve_df = pd.DataFrame(sleeve_results)\n", + "print(f\"\\nTotal combos: {len(sleeve_df)}\")" + ], "id": "cell-64086bd7" }, { @@ -203,7 +299,43 @@ ] } ], - "source": "# Section 5: Ensemble (equal-weight) and fee sweep\nensemble_results = []\n\nfor symbol, hist in crypto_data.items():\n close = hist[\"close\"].values\n \n for fee in fee_levels:\n _, rets_k60 = kelly_sleeve(close, cap=0.6, fee_bps=fee)\n _, rets_k30 = kelly_sleeve(close, cap=0.3, fee_bps=fee)\n _, rets_vth = vol_targeting_sleeve(close, target_vol=0.15, fee_bps=fee)\n \n min_len = min(len(rets_k60), len(rets_k30), len(rets_vth))\n ensemble_rets = (rets_k60[:min_len] + rets_k30[:min_len] + rets_vth[:min_len]) / 3\n \n sharpe_ensemble = np.mean(ensemble_rets) / (np.std(ensemble_rets) + 1e-10) * np.sqrt(252)\n \n # BH baseline (round-trip fee)\n bh_returns = np.diff(np.log(close))[:min_len]\n bh_net = bh_returns - 2 * fee / 10000\n sharpe_bh = np.mean(bh_net) / (np.std(bh_net) + 1e-10) * np.sqrt(252)\n \n ensemble_results.append({\n \"coin\": symbol,\n \"fee_bps\": fee,\n \"sharpe_ensemble\": sharpe_ensemble,\n \"sharpe_bh\": sharpe_bh,\n \"delta_sharpe\": sharpe_ensemble - sharpe_bh,\n \"beats_bh\": int(sharpe_ensemble > sharpe_bh),\n })\n \n print(f\"{symbol} @ {fee}bps: Ensemble={sharpe_ensemble:.3f}, BH={sharpe_bh:.3f}, \"\n f\"delta={sharpe_ensemble - sharpe_bh:+.3f}\")\n\nensemble_df = pd.DataFrame(ensemble_results)\nprint(f\"\\nTotal combos: {len(ensemble_df)}\")", + "source": [ + "# Section 5: Ensemble (equal-weight) and fee sweep\n", + "ensemble_results = []\n", + "\n", + "for symbol, hist in crypto_data.items():\n", + " close = hist[\"close\"].values\n", + " \n", + " for fee in fee_levels:\n", + " _, rets_k60 = kelly_sleeve(close, cap=0.6, fee_bps=fee)\n", + " _, rets_k30 = kelly_sleeve(close, cap=0.3, fee_bps=fee)\n", + " _, rets_vth = vol_targeting_sleeve(close, target_vol=0.15, fee_bps=fee)\n", + " \n", + " min_len = min(len(rets_k60), len(rets_k30), len(rets_vth))\n", + " ensemble_rets = (rets_k60[:min_len] + rets_k30[:min_len] + rets_vth[:min_len]) / 3\n", + " \n", + " sharpe_ensemble = np.mean(ensemble_rets) / (np.std(ensemble_rets) + 1e-10) * np.sqrt(252)\n", + " \n", + " # BH baseline (round-trip fee)\n", + " bh_returns = np.diff(np.log(close))[:min_len]\n", + " bh_net = bh_returns - 2 * fee / 10000\n", + " sharpe_bh = np.mean(bh_net) / (np.std(bh_net) + 1e-10) * np.sqrt(252)\n", + " \n", + " ensemble_results.append({\n", + " \"coin\": symbol,\n", + " \"fee_bps\": fee,\n", + " \"sharpe_ensemble\": sharpe_ensemble,\n", + " \"sharpe_bh\": sharpe_bh,\n", + " \"delta_sharpe\": sharpe_ensemble - sharpe_bh,\n", + " \"beats_bh\": int(sharpe_ensemble > sharpe_bh),\n", + " })\n", + " \n", + " print(f\"{symbol} @ {fee}bps: Ensemble={sharpe_ensemble:.3f}, BH={sharpe_bh:.3f}, \"\n", + " f\"delta={sharpe_ensemble - sharpe_bh:+.3f}\")\n", + "\n", + "ensemble_df = pd.DataFrame(ensemble_results)\n", + "print(f\"\\nTotal combos: {len(ensemble_df)}\")" + ], "id": "cell-9a420cbf" }, { diff --git a/MyIA.AI.Notebooks/QuantConnect/research/research_rl_multi_asset.ipynb b/MyIA.AI.Notebooks/QuantConnect/research/research_rl_multi_asset.ipynb index 1f063fe0c7..100bf9735c 100644 --- a/MyIA.AI.Notebooks/QuantConnect/research/research_rl_multi_asset.ipynb +++ b/MyIA.AI.Notebooks/QuantConnect/research/research_rl_multi_asset.ipynb @@ -149,7 +149,92 @@ ] } ], - "source": "class PortfolioEnv:\n \"\"\"Multi-asset portfolio environment with continuous actions (weights).\n\n State: flattened LOOKBACK x N_ASSETS return matrix.\n Action: N_ASSETS logits (applied softmax to get weights).\n Reward: rolling Sharpe of portfolio - rolling Sharpe of equal-weight.\n \"\"\"\n\n def __init__(self, returns, lookback=LOOKBACK, episode_len=EPISODE_LEN,\n fee_bps=FEE_BPS, n_assets=n_assets):\n self.returns = returns\n self.lookback = lookback\n self.episode_len = episode_len\n self.fee = fee_bps / 10000\n self.n_assets = n_assets\n self.ew_weight = np.ones(n_assets) / n_assets\n\n @property\n def state_dim(self):\n return self.lookback * self.n_assets\n\n @property\n def action_dim(self):\n return self.n_assets\n\n def reset(self, start_idx=None):\n n = len(self.returns) - self.lookback - 2\n if start_idx is None:\n start_idx = np.random.randint(0, max(1, n - self.episode_len))\n self.t = start_idx + self.lookback\n self.end = min(self.t + self.episode_len, len(self.returns) - 1)\n self.prev_weights = self.ew_weight.copy()\n self.port_returns = []\n self.ew_returns = []\n return self._get_state()\n\n def _get_state(self):\n window = self.returns[self.t - self.lookback:self.t] # (L, A)\n # Normalize each asset's returns\n std = np.std(window, axis=0, keepdims=True) + 1e-8\n normalized = window / std\n return normalized.flatten()\n\n def step(self, weights):\n \"\"\"weights: array of shape (n_assets,), sums to ~1.\"\"\"\n r = self.returns[self.t] # (A,)\n\n # Portfolio return\n port_ret = np.dot(weights, r)\n\n # Equal-weight return\n ew_ret = np.mean(r)\n\n # Transaction cost: L1 change in weights\n turnover = np.sum(np.abs(weights - self.prev_weights))\n fee_cost = turnover * self.fee * 0.5 # half-spread model\n\n port_ret_net = port_ret - fee_cost\n\n self.port_returns.append(port_ret_net)\n self.ew_returns.append(ew_ret)\n\n # Reward: differential Sharpe (rolling)\n reward = self._compute_reward(port_ret_net, ew_ret)\n\n self.prev_weights = weights.copy()\n self.t += 1\n done = self.t >= self.end\n\n next_state = self._get_state() if not done else np.zeros(self.state_dim)\n return next_state, reward, done, {\"port_ret\": port_ret_net, \"ew_ret\": ew_ret, \"turnover\": turnover}\n\n def _compute_reward(self, port_ret, ew_ret):\n if len(self.port_returns) < 5:\n return port_ret - ew_ret # simple differential\n window_p = np.array(self.port_returns[-SHARPE_WINDOW:])\n window_e = np.array(self.ew_returns[-SHARPE_WINDOW:])\n sp = np.mean(window_p) / (np.std(window_p) + 1e-8) * np.sqrt(252)\n se = np.mean(window_e) / (np.std(window_e) + 1e-8) * np.sqrt(252)\n return (sp - se) * 0.01 # scaled differential Sharpe\n\n\nprint(f\"PortfolioEnv: state_dim={LOOKBACK * n_assets}, action_dim={n_assets}\")\nprint(f\"Baseline: equal-weight ({n_assets} assets, 1/{n_assets:.4f} each)\")" + "source": [ + "class PortfolioEnv:\n", + " \"\"\"Multi-asset portfolio environment with continuous actions (weights).\n", + "\n", + " State: flattened LOOKBACK x N_ASSETS return matrix.\n", + " Action: N_ASSETS logits (applied softmax to get weights).\n", + " Reward: rolling Sharpe of portfolio - rolling Sharpe of equal-weight.\n", + " \"\"\"\n", + "\n", + " def __init__(self, returns, lookback=LOOKBACK, episode_len=EPISODE_LEN,\n", + " fee_bps=FEE_BPS, n_assets=n_assets):\n", + " self.returns = returns\n", + " self.lookback = lookback\n", + " self.episode_len = episode_len\n", + " self.fee = fee_bps / 10000\n", + " self.n_assets = n_assets\n", + " self.ew_weight = np.ones(n_assets) / n_assets\n", + "\n", + " @property\n", + " def state_dim(self):\n", + " return self.lookback * self.n_assets\n", + "\n", + " @property\n", + " def action_dim(self):\n", + " return self.n_assets\n", + "\n", + " def reset(self, start_idx=None):\n", + " n = len(self.returns) - self.lookback - 2\n", + " if start_idx is None:\n", + " start_idx = np.random.randint(0, max(1, n - self.episode_len))\n", + " self.t = start_idx + self.lookback\n", + " self.end = min(self.t + self.episode_len, len(self.returns) - 1)\n", + " self.prev_weights = self.ew_weight.copy()\n", + " self.port_returns = []\n", + " self.ew_returns = []\n", + " return self._get_state()\n", + "\n", + " def _get_state(self):\n", + " window = self.returns[self.t - self.lookback:self.t] # (L, A)\n", + " # Normalize each asset's returns\n", + " std = np.std(window, axis=0, keepdims=True) + 1e-8\n", + " normalized = window / std\n", + " return normalized.flatten()\n", + "\n", + " def step(self, weights):\n", + " \"\"\"weights: array of shape (n_assets,), sums to ~1.\"\"\"\n", + " r = self.returns[self.t] # (A,)\n", + "\n", + " # Portfolio return\n", + " port_ret = np.dot(weights, r)\n", + "\n", + " # Equal-weight return\n", + " ew_ret = np.mean(r)\n", + "\n", + " # Transaction cost: L1 change in weights\n", + " turnover = np.sum(np.abs(weights - self.prev_weights))\n", + " fee_cost = turnover * self.fee * 0.5 # half-spread model\n", + "\n", + " port_ret_net = port_ret - fee_cost\n", + "\n", + " self.port_returns.append(port_ret_net)\n", + " self.ew_returns.append(ew_ret)\n", + "\n", + " # Reward: differential Sharpe (rolling)\n", + " reward = self._compute_reward(port_ret_net, ew_ret)\n", + "\n", + " self.prev_weights = weights.copy()\n", + " self.t += 1\n", + " done = self.t >= self.end\n", + "\n", + " next_state = self._get_state() if not done else np.zeros(self.state_dim)\n", + " return next_state, reward, done, {\"port_ret\": port_ret_net, \"ew_ret\": ew_ret, \"turnover\": turnover}\n", + "\n", + " def _compute_reward(self, port_ret, ew_ret):\n", + " if len(self.port_returns) < 5:\n", + " return port_ret - ew_ret # simple differential\n", + " window_p = np.array(self.port_returns[-SHARPE_WINDOW:])\n", + " window_e = np.array(self.ew_returns[-SHARPE_WINDOW:])\n", + " sp = np.mean(window_p) / (np.std(window_p) + 1e-8) * np.sqrt(252)\n", + " se = np.mean(window_e) / (np.std(window_e) + 1e-8) * np.sqrt(252)\n", + " return (sp - se) * 0.01 # scaled differential Sharpe\n", + "\n", + "\n", + "print(f\"PortfolioEnv: state_dim={LOOKBACK * n_assets}, action_dim={n_assets}\")\n", + "print(f\"Baseline: equal-weight ({n_assets} assets, 1/{n_assets:.4f} each)\")" + ] }, { "cell_type": "markdown", diff --git a/MyIA.AI.Notebooks/RL/rl_12_distributional_rl.ipynb b/MyIA.AI.Notebooks/RL/rl_12_distributional_rl.ipynb index 6c68e74c32..93261d6030 100644 --- a/MyIA.AI.Notebooks/RL/rl_12_distributional_rl.ipynb +++ b/MyIA.AI.Notebooks/RL/rl_12_distributional_rl.ipynb @@ -761,7 +761,15 @@ { "cell_type": "markdown", "id": "ffe2297b", - "source": "### Lecture : que nous apprend la distribution apprise ?\n\nLa politique greedy choisit **l'action 1 (droite)** : c'est simplement l'action dont l'espérance $\\mathbb{E}[Z]$ est la plus élevée — la ligne rouge pointillée sur chaque sous-figure. Un DQN scalaire aurait pris la **même** décision ; jusqu'ici, rien de neuf.\n\nLa différence est dans les **barres**. Pour chaque action, C51 expose la *forme complète* du retour : sa concentration, sa dispersion, son asymétrie, ses éventuels modes. L'espérance (la ligne rouge) résume cette distribution en un seul nombre et **jette tout le reste**. Or deux distributions de même moyenne peuvent avoir des risques radicalement différents : l'une concentrée autour de sa moyenne (retour prévisible), l'autre bimodale ou étalée (jackpot ou effondrement). Un DQN les verrait comme **identiques** ; C51 les distingue.\n\nC'est précisément cette information que l'**Exercice 3** exploite : au lieu de maximiser l'espérance, on peut maximiser la **CVaR** (la moyenne conditionnelle des pires scénarios) et obtenir une politique *averse au risque* — strictement impossible avec un scalaire $Q$. La section suivante explique pourquoi, empiriquement, ce signal d'apprentissage plus riche stabilise aussi l'entraînement.", + "source": [ + "### Lecture : que nous apprend la distribution apprise ?\n", + "\n", + "La politique greedy choisit **l'action 1 (droite)** : c'est simplement l'action dont l'espérance $\\mathbb{E}[Z]$ est la plus élevée — la ligne rouge pointillée sur chaque sous-figure. Un DQN scalaire aurait pris la **même** décision ; jusqu'ici, rien de neuf.\n", + "\n", + "La différence est dans les **barres**. Pour chaque action, C51 expose la *forme complète* du retour : sa concentration, sa dispersion, son asymétrie, ses éventuels modes. L'espérance (la ligne rouge) résume cette distribution en un seul nombre et **jette tout le reste**. Or deux distributions de même moyenne peuvent avoir des risques radicalement différents : l'une concentrée autour de sa moyenne (retour prévisible), l'autre bimodale ou étalée (jackpot ou effondrement). Un DQN les verrait comme **identiques** ; C51 les distingue.\n", + "\n", + "C'est précisément cette information que l'**Exercice 3** exploite : au lieu de maximiser l'espérance, on peut maximiser la **CVaR** (la moyenne conditionnelle des pires scénarios) et obtenir une politique *averse au risque* — strictement impossible avec un scalaire $Q$. La section suivante explique pourquoi, empiriquement, ce signal d'apprentissage plus riche stabilise aussi l'entraînement." + ], "metadata": {} }, { diff --git a/MyIA.AI.Notebooks/Sudoku/Sudoku-14-BDD-Csharp.ipynb b/MyIA.AI.Notebooks/Sudoku/Sudoku-14-BDD-Csharp.ipynb index 80bf03ab34..6921b6974a 100644 --- a/MyIA.AI.Notebooks/Sudoku/Sudoku-14-BDD-Csharp.ipynb +++ b/MyIA.AI.Notebooks/Sudoku/Sudoku-14-BDD-Csharp.ipynb @@ -356,7 +356,15 @@ "text": "Exercice a completer\r\n" } ], - "source": "// EXERCICE : Construire un BDD pour la contrainte \"exactement un parmi N\"\npublic BDDNode ExactlyOneOfN(BDDNode[] variables)\n{\n // TODO: Construisez un BDD qui est vrai si exactement une des N variables est vraie\n return null; // TODO etudiant\n}\nConsole.WriteLine(\"Exercice a completer\");", + "source": [ + "// EXERCICE : Construire un BDD pour la contrainte \"exactement un parmi N\"\n", + "public BDDNode ExactlyOneOfN(BDDNode[] variables)\n", + "{\n", + " // TODO: Construisez un BDD qui est vrai si exactement une des N variables est vraie\n", + " return null; // TODO etudiant\n", + "}\n", + "Console.WriteLine(\"Exercice a completer\");" + ], "id": "cell-a18373c4" }, { @@ -1111,7 +1119,16 @@ "text": "Exercice a completer\r\n" } ], - "source": "// EXERCICE : Construire un MDD pour la contrainte \"tous différents\"\npublic MDDNode BuildAllDifferentMDD(int numCells, int maxValue)\n{\n // TODO: Construisez un MDD qui accepte uniquement les assignments\n // ou toutes les valeurs sont différentes\n return null; // TODO etudiant\n}\nConsole.WriteLine(\"Exercice a completer\");", + "source": [ + "// EXERCICE : Construire un MDD pour la contrainte \"tous différents\"\n", + "public MDDNode BuildAllDifferentMDD(int numCells, int maxValue)\n", + "{\n", + " // TODO: Construisez un MDD qui accepte uniquement les assignments\n", + " // ou toutes les valeurs sont différentes\n", + " return null; // TODO etudiant\n", + "}\n", + "Console.WriteLine(\"Exercice a completer\");" + ], "id": "cell-fff6d6dd" }, { diff --git a/scripts/notebook_tools/twin_pairs.d/gametheory-10-forwardinduction-spe.yaml b/scripts/notebook_tools/twin_pairs.d/gametheory-10-forwardinduction-spe.yaml index 40b0163059..19e6fb323d 100644 --- a/scripts/notebook_tools/twin_pairs.d/gametheory-10-forwardinduction-spe.yaml +++ b/scripts/notebook_tools/twin_pairs.d/gametheory-10-forwardinduction-spe.yaml @@ -6,6 +6,7 @@ bridge_verdict: SOTA-OK bridge_verdict_reason: "Modele #10382 solver-free par conception : theorie des jeux algorithmique enseignee from-scratch des deux cotes (l algorithme - induction arriere, existence Nash, SPE, formes extensive/combinatoire/cooperative - EST la lecon ; il n existe pas de bibliotheque .NET canonique de solveur de jeu a invoquer, contrairement aux paires lib-vs-lib comme PyMC/Infer.NET ou scikit/ML.NET). Les deux cotes atteignent le SOTA de leur ecosysteme pour cet objectif pedagogique : reimplementation didactique du formalisme mathematique. Pair verifiee firsthand (census moteur : CS=scratch, PY=scratch)." known_differences: + - "Re-baseline 2026-09-11 (myia-po-2023:CoursIA, #15446 / chore #15429) : suite de la normalisation str->list des cellules `source` (representation seule, aucun contenu pedagogique touche) l'attestation a ete refaite a l'ordinal 0012, au-dessus du 0011 venu de main (#15449, T8) qui portait l'etat pre-normalisation sous un ordinal superieur et masquait celle de cette PR — collision d'ordinal diagnostiquee firsthand par ai-01 sur #15446 ; l'ancien 0010 de cette PR, en collision avec le 0010-2026-09-08 de main, est retire. Verifie firsthand : le SHA canonique du carnet (toutes listes-de-chaines jointes, metadata de niveau carnet exclue) est IDENTIQUE base vs head, et les champs `text` des sorties sont inchanges ; seul le content_sha du registre bouge. Le garde #8057 demande une attestation de parite, pas une reparation." - "2026-08-22 (po-2025) #12349 tranche A (PR #12356) : cote C# uniquement -- chemin Gambit code en dur (Program Files (x86) + throw) remplace par le helper FindGambitEfg() (probe GAMBIT_HOME puis candidats Windows du pattern GT-5, puis Unix /usr/bin /usr/local/bin /opt/homebrew/bin sans suffixe .exe), cote C# re-execute 14/14 (gambit-lcp trouve toujours 1 equilibre, outputs frais), cote Python inchange. Changement d'infrastructure multiplateforme #10643, aucune prose/socle pedagogique touche -- parite semantique intacte." - "2026-08-17 (po-2026) re-exec seq etage 3 #11112 (PR #11347) : cote python re-execute (sequences 1..N propres, outputs frais, sources byte-identiques), cote C# inchange. Aucune prose ajoutee -- parite semantique intacte, seuls les blob SHA ont bouge." - "Socle pedagogique commun : les raffinements d'equilibre au-dela du SPE (Selten 1965) -- sous-jeux et equilibre parfait, menaces et promesses credibles, induction avant (forward induction), perfection en mains tremblantes (trembling-hand perfection, Selten 1975), application canonique du jeu du « Burn Money » (signal costly), comparaison des raffinements. Distinct du GT-9 (induction arriere + paradoxes) : le GT-10 porte sur les RAFFINEMENTS de la notion d'equilibre (forward induction, trembling-hand), pas sur l'algorithme de resolution." diff --git a/scripts/notebook_tools/twin_pairs.d/gametheory-10-forwardinduction-spe/0012-2026-09-11-myia-po-2023-CoursIA.yaml b/scripts/notebook_tools/twin_pairs.d/gametheory-10-forwardinduction-spe/0012-2026-09-11-myia-po-2023-CoursIA.yaml new file mode 100644 index 0000000000..dca99d09c4 --- /dev/null +++ b/scripts/notebook_tools/twin_pairs.d/gametheory-10-forwardinduction-spe/0012-2026-09-11-myia-po-2023-CoursIA.yaml @@ -0,0 +1,6 @@ +date: '2026-09-11' +by: myia-po-2023:CoursIA +python_sha: 52e983924607fcad4d797cf092f9adfa55f225fd +csharp_sha: 11c2595eaac6c95201712789073e19e4731694a0 +content_python_sha: 41aacfc35d4184ae63dadb1345ee8f8027f33ef795ce376da9e49e2f54c2989d +content_csharp_sha: af0b6d8d123fa15c25af1bf8ab4b9de2b9c33d876b9c14b54f8fe55279ffbc58 diff --git a/scripts/notebook_tools/twin_pairs.d/sudoku-14-bdd.yaml b/scripts/notebook_tools/twin_pairs.d/sudoku-14-bdd.yaml index b49d5eb8fa..58fc2b05ea 100644 --- a/scripts/notebook_tools/twin_pairs.d/sudoku-14-bdd.yaml +++ b/scripts/notebook_tools/twin_pairs.d/sudoku-14-bdd.yaml @@ -6,6 +6,7 @@ bridge_verdict: SOTA-OK bridge_verdict_reason: "Solver-free by design : les deux jumeaux implementent les BDD/MDD (Binary/Multi-valued Decision Diagrams) from-scratch en pure stdlib. Python = `__future__/dataclasses/time/typing` uniquement (zero solveur externe, confirme par grep `import`/`from` = {`__future__`, `dataclasses`, `time`, `typing`}, aucune lib BDD tierce). C# = `System.Collections.Generic/System.Diagnostics/System.Linq` uniquement (zero solveur externe, confirme par grep `using` = {`System.Collections.Generic`, `System.Diagnostics`, `System.Linq`}, aucune lib BDD tierce). Verifie firsthand (c.235 audit 2026-08-12, myia-po-2023:CoursIA-2) : 33 cells Python / 40 cells C#, divergence d'implementation documentee dans known_differences (Python hand-roll, C# hand-roll avec idiomes differents). Pas de moteur SOTA a brancher : les BDD sont la demonstration (construction recursive de l'arbre de decisions, application des contraintes Sudoku comme noeuds BDD, evaluation par reduction). Verdict SOTA-OK : solver-free by design des deux cotes, aucune asymetrie de machinerie a corriger. parity_level `semantic` confirmee par audit (meme concept BDD, implementation differente)." known_differences: + - "Re-baseline 2026-09-11 (myia-po-2023:CoursIA, #15446 / chore #15429) : suite de la normalisation str->list des cellules `source` (representation seule, aucun contenu pedagogique touche) l'attestation a ete refaite a l'ordinal 0012, au-dessus du 0011 venu de main (#15449, T8) qui portait l'etat pre-normalisation sous un ordinal superieur et masquait celle de cette PR — collision d'ordinal diagnostiquee firsthand par ai-01 sur #15446 ; l'ancien 0010 de cette PR, en collision avec le 0010-2026-09-08 de main, est retire. Verifie firsthand : le SHA canonique du carnet (toutes listes-de-chaines jointes, metadata de niveau carnet exclue) est IDENTIQUE base vs head, et les champs `text` des sorties sont inchanges ; seul le content_sha du registre bouge. Le garde #8057 demande une attestation de parite, pas une reparation." - "2026-09-02 (grain hr-separateurs famille Sudoku, myia-po-2027:CoursIA) : conversion des separateurs markdown --- vers *** via fix_hr_separator.py (incident #11451 : un --- seul ouvre un bloc YAML Pandoc et casse quarto render) -- jumeau C# seul (13 separateurs). Cellules markdown uniquement, aucune source de code ni output modifiee, pas de re-exec (C.2 exception markdown-only). Les blob et content SHAs deplaces dans l audit ci-dessus sont des artefacts de cette conversion markdown (le content_sha hashe le JSON canonique des cellules), pas une divergence pedagogique." - "Socle pedagogique commun : resolution du Sudoku par automates a decision binaire (BDD / MDD, Binary/Multi-valued Decision Diagrams), meme concept (codage booleen des contraintes, reduction canonique du graphe de decision, satisfaction via parcours). Le notebook Python declare explicitement etre le jumeau du C# (« Jumeau Python du notebook Sudoku-14-BDD-Csharp.ipynb »)." - "Divergence d'implementation : les deux cotes hand-rollent les BDD/MDD from scratch (pas de lib BDD tierce) -- le Python via dataclasses/typing, le C# via System.Collections.Generic + System.Linq. Meme reconstruction pedagogique de l'automate, idiomes de langage differents." diff --git a/scripts/notebook_tools/twin_pairs.d/sudoku-14-bdd/0012-2026-09-11-myia-po-2023-CoursIA.yaml b/scripts/notebook_tools/twin_pairs.d/sudoku-14-bdd/0012-2026-09-11-myia-po-2023-CoursIA.yaml new file mode 100644 index 0000000000..1bb955404f --- /dev/null +++ b/scripts/notebook_tools/twin_pairs.d/sudoku-14-bdd/0012-2026-09-11-myia-po-2023-CoursIA.yaml @@ -0,0 +1,6 @@ +date: '2026-09-11' +by: myia-po-2023:CoursIA +python_sha: 8672bbe7b5508c8dbca73e17d6d1a0af2f68ab00 +csharp_sha: 6921b6974a72d9cc3d05f6e2fe96dce5d49ec3ae +content_python_sha: 1bfc5722a332acb62b38fc19af0f8a32cdddb604b0867e3949607b1897c9db07 +content_csharp_sha: 5504c03af3e6ad940c6ad20bf3ded4739c3e21643c411921eed985da1884df2c