commit 76549424fe62d4fdb752e3f50b9172a7c5f25a19 Author: St.jess777 Date: Fri Aug 29 11:10:28 2025 +0200 Initial Streamlit Cloud deploy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b65fa6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.venv/ +venv/ +ENV/ +*.egg-info/ + +# OS +.DS_Store +Thumbs.db + +# Local outputs +reports/ +*.xlsx +*.csv diff --git a/archive/inactive_reports/backtest_results b/archive/inactive_reports/backtest_results new file mode 100644 index 0000000..e69de29 diff --git a/archive/legacy_notebooks/%change_heatmap.ipynb b/archive/legacy_notebooks/%change_heatmap.ipynb new file mode 100644 index 0000000..9403960 --- /dev/null +++ b/archive/legacy_notebooks/%change_heatmap.ipynb @@ -0,0 +1,135 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "1 Failed download:\n", + "['SGDCAD=X']: HTTPError('HTTP Error 404: ')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[📊] FX % Change Heatmap for 2025-07-04\n", + " USD CAD EUR GBP JPY AUD NZD CHF SGD NOK\n", + "USD NaN NaN -0.19 0.00 -0.31 0.24 0.22 NaN -0.07 0.15\n", + "CAD -0.11 NaN -0.30 -0.13 -0.40 0.17 0.10 NaN -0.22 0.03\n", + "EUR 0.19 0.31 NaN 0.18 NaN NaN 0.40 NaN 0.13 NaN\n", + "GBP 0.00 NaN -0.18 NaN -0.31 NaN NaN NaN NaN 0.08\n", + "JPY 0.30 0.43 0.13 0.32 NaN 0.61 0.52 0.26 NaN 0.94\n", + "AUD -0.24 NaN -0.44 -0.22 NaN NaN NaN NaN NaN -0.18\n", + "NZD -0.22 NaN -0.53 -0.23 -0.53 0.09 NaN NaN -0.28 NaN\n", + "CHF NaN 0.36 0.03 0.20 NaN 0.50 0.43 NaN 0.48 0.32\n", + "SGD 0.07 NaN -0.07 0.11 -0.09 0.40 0.33 0.14 NaN 0.20\n", + "NOK NaN NaN -0.31 -0.10 -0.34 NaN NaN 0.06 NaN NaN\n", + "[💾] Exported FX heatmap to: reports/fx_major_heatmap.xlsx\n" + ] + } + ], + "source": [ + "import yfinance as yf\n", + "import pandas as pd\n", + "import numpy as np\n", + "import os\n", + "from datetime import datetime\n", + "\n", + "# === CONFIG ===\n", + "EXPORT_TO_EXCEL = True\n", + "OUTPUT_FILE = \"reports/fx_major_heatmap.xlsx\"\n", + "CURRENCY_LIST = ['USD','CAD', 'EUR', 'GBP', 'JPY', 'AUD', 'NZD', 'CHF','SGD','NOK']\n", + "TODAY_DATE = datetime.utcnow().strftime('%Y-%m-%d')\n", + "\n", + "# === Initialize matrix ===\n", + "matrix = pd.DataFrame(index=CURRENCY_LIST, columns=CURRENCY_LIST, dtype=float)\n", + "\n", + "def get_daily_pct_change(ticker):\n", + " try:\n", + " data = yf.download(ticker, period=\"2d\", interval=\"1d\", progress=False)\n", + " if len(data) < 2:\n", + " return None\n", + " open_val = data['Open'].iloc[-1].item()\n", + " close_val = data['Close'].iloc[-1].item()\n", + " return (close_val - open_val) / open_val * 100\n", + " except Exception as e:\n", + " print(f\"[âš ī¸] Error fetching {ticker}: {e}\")\n", + " return None\n", + "\n", + "# === Build matrix ===\n", + "for base in CURRENCY_LIST:\n", + " for quote in CURRENCY_LIST:\n", + " if base == quote:\n", + " matrix.at[base, quote] = np.nan\n", + " continue\n", + " pair = f\"{base}{quote}=X\"\n", + " pct_change = get_daily_pct_change(pair)\n", + " if pct_change is not None:\n", + " matrix.at[base, quote] = round(pct_change, 2)\n", + "\n", + "# === Display matrix ===\n", + "print(f\"[📊] FX % Change Heatmap for {TODAY_DATE}\")\n", + "print(matrix)\n", + "\n", + "# === Export to Excel (optional) ===\n", + "if EXPORT_TO_EXCEL:\n", + " os.makedirs(\"reports\", exist_ok=True)\n", + " with pd.ExcelWriter(OUTPUT_FILE, engine='xlsxwriter') as writer:\n", + " matrix.to_excel(writer, sheet_name='Heatmap')\n", + " workbook = writer.book\n", + " worksheet = writer.sheets['Heatmap']\n", + " fmt = workbook.add_format({'num_format': '0.00', 'align': 'center'})\n", + "\n", + " # Apply conditional formatting\n", + " worksheet.conditional_format('B2:I9', {\n", + " 'type': '3_color_scale',\n", + " 'min_color': \"#63BE7B\", # green\n", + " 'mid_color': \"#FFEB84\", # yellow\n", + " 'max_color': \"#F8696B\", # red\n", + " })\n", + " print(f\"[💾] Exported FX heatmap to: {OUTPUT_FILE}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/archive/legacy_notebooks/.ipynb_checkpoints/%change_heatmap-checkpoint.ipynb b/archive/legacy_notebooks/.ipynb_checkpoints/%change_heatmap-checkpoint.ipynb new file mode 100644 index 0000000..ae4733a --- /dev/null +++ b/archive/legacy_notebooks/.ipynb_checkpoints/%change_heatmap-checkpoint.ipynb @@ -0,0 +1,142 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "YF.download() has changed argument auto_adjust default to True\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "1 Failed download:\n", + "['SGDCAD=X']: HTTPError('HTTP Error 404: ')\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[📊] FX % Change Heatmap for 2025-07-04\n", + " USD CAD EUR GBP JPY AUD NZD CHF SGD NOK\n", + "USD NaN 0.03 -0.19 -0.03 -0.40 0.18 0.08 -0.15 -0.11 0.21\n", + "CAD -0.06 NaN -0.22 -0.07 -0.44 0.14 0.03 -0.10 -0.19 0.15\n", + "EUR 0.19 0.25 NaN 0.16 -0.21 0.39 0.27 0.09 0.08 0.33\n", + "GBP 0.03 0.11 -0.13 NaN -0.38 0.26 0.10 -0.11 -0.07 0.16\n", + "JPY 0.40 0.45 0.23 0.38 NaN 0.60 0.48 0.38 NaN 1.12\n", + "AUD -0.19 -0.10 -0.36 -0.12 -0.53 NaN -0.11 -0.23 -0.24 -0.05\n", + "NZD -0.08 0.04 -0.27 -0.09 -0.48 0.15 NaN 0.05 -0.17 NaN\n", + "CHF 0.16 0.26 0.00 0.16 -0.24 0.38 0.27 NaN 0.43 0.34\n", + "SGD 0.11 NaN -0.03 0.13 -0.15 0.35 0.23 0.19 NaN 0.28\n", + "NOK -0.21 NaN -0.34 -0.16 -0.45 NaN NaN 0.03 NaN NaN\n", + "[💾] Exported FX heatmap to: reports/fx_major_heatmap.xlsx\n" + ] + } + ], + "source": [ + "import yfinance as yf\n", + "import pandas as pd\n", + "import numpy as np\n", + "import os\n", + "from datetime import datetime\n", + "\n", + "# === CONFIG ===\n", + "EXPORT_TO_EXCEL = True\n", + "OUTPUT_FILE = \"reports/fx_major_heatmap.xlsx\"\n", + "CURRENCY_LIST = ['USD','CAD', 'EUR', 'GBP', 'JPY', 'AUD', 'NZD', 'CHF','SGD','NOK']\n", + "TODAY_DATE = datetime.utcnow().strftime('%Y-%m-%d')\n", + "\n", + "# === Initialize matrix ===\n", + "matrix = pd.DataFrame(index=CURRENCY_LIST, columns=CURRENCY_LIST, dtype=float)\n", + "\n", + "def get_daily_pct_change(ticker):\n", + " try:\n", + " data = yf.download(ticker, period=\"2d\", interval=\"1d\", progress=False)\n", + " if len(data) < 2:\n", + " return None\n", + " open_val = data['Open'].iloc[-1].item()\n", + " close_val = data['Close'].iloc[-1].item()\n", + " return (close_val - open_val) / open_val * 100\n", + " except Exception as e:\n", + " print(f\"[âš ī¸] Error fetching {ticker}: {e}\")\n", + " return None\n", + "\n", + "# === Build matrix ===\n", + "for base in CURRENCY_LIST:\n", + " for quote in CURRENCY_LIST:\n", + " if base == quote:\n", + " matrix.at[base, quote] = np.nan\n", + " continue\n", + " pair = f\"{base}{quote}=X\"\n", + " pct_change = get_daily_pct_change(pair)\n", + " if pct_change is not None:\n", + " matrix.at[base, quote] = round(pct_change, 2)\n", + "\n", + "# === Display matrix ===\n", + "print(f\"[📊] FX % Change Heatmap for {TODAY_DATE}\")\n", + "print(matrix)\n", + "\n", + "# === Export to Excel (optional) ===\n", + "if EXPORT_TO_EXCEL:\n", + " os.makedirs(\"reports\", exist_ok=True)\n", + " with pd.ExcelWriter(OUTPUT_FILE, engine='xlsxwriter') as writer:\n", + " matrix.to_excel(writer, sheet_name='Heatmap')\n", + " workbook = writer.book\n", + " worksheet = writer.sheets['Heatmap']\n", + " fmt = workbook.add_format({'num_format': '0.00', 'align': 'center'})\n", + "\n", + " # Apply conditional formatting\n", + " worksheet.conditional_format('B2:I9', {\n", + " 'type': '3_color_scale',\n", + " 'min_color': \"#63BE7B\", # green\n", + " 'mid_color': \"#FFEB84\", # yellow\n", + " 'max_color': \"#F8696B\", # red\n", + " })\n", + " print(f\"[💾] Exported FX heatmap to: {OUTPUT_FILE}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/archive/legacy_notebooks/.ipynb_checkpoints/Top_movers_X-checkpoint.ipynb b/archive/legacy_notebooks/.ipynb_checkpoints/Top_movers_X-checkpoint.ipynb new file mode 100644 index 0000000..6c205a3 --- /dev/null +++ b/archive/legacy_notebooks/.ipynb_checkpoints/Top_movers_X-checkpoint.ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[💾] Extension alerts logged to: reports/extension_alert_log.csv\n", + "[✅] Unusual Extension Movers:\n", + " Ticker Today % Change Avg % Change Std Dev Z-Score Timestamp\n", + "EURNOK=X 0.53 0.0 0.31 1.68 2025-06-11 14:31:07\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "import yfinance as yf\n", + "import pandas as pd\n", + "import os\n", + "\n", + "\n", + "# Parameters\n", + "TICKER_LIST = [\n", + " 'USDCAD=X', 'USDGBP=X', 'USDNOK=X', 'USDPLN=X', 'USDAUD=X', 'USDSGD=X',\n", + " 'USDJPY=X', 'USDZAR=X', 'USDBRL=X', 'EURUSD=X', 'EURGBP=X', 'EURCHF=X',\n", + " 'EURPLN=X', 'EURCZK=X', 'EURNZD=X', 'EURSEK=X', 'EURZAR=X', 'EURSGD=X',\n", + " 'GBPNOK=X', 'GBPJPY=X', 'GBPAUD=X', 'GBPCAD=X', 'SEKNOK=X', 'SEKJPY=X',\n", + " 'CHFNOK=X', 'CADNOK=X', 'AUDNZD=X', 'AUDJPY=X', 'AUDSEK=X', 'AUDCAD=X',\n", + " 'NZDSGD=X', 'NZDCHF=X', 'NZDNOK=X', 'SGDJPY=X', 'SGDHKD=X', 'EURCAD=X',\n", + " 'USDCHF=X', 'GBPCHF=X', 'EURNOK=X'\n", + "] # Add more tickers\n", + "lookback_days = 10\n", + "std_threshold = 1.5 # Flag moves above 1.5x std deviation\n", + "\n", + "def get_unusual_movers(tickers, lookback_days, std_threshold):\n", + " unusual_movers = []\n", + "\n", + " for ticker in tickers:\n", + " data = yf.download(ticker, period=f\"{lookback_days + 2}d\", interval='1d')\n", + " data['Pct Change'] = data['Close'].pct_change() * 100\n", + " \n", + " recent_changes = data['Pct Change'].iloc[-(lookback_days+1):-1] # Exclude today\n", + " today_change = data['Pct Change'].iloc[-1]\n", + "\n", + " avg = recent_changes.mean()\n", + " std = recent_changes.std()\n", + "\n", + " if abs(today_change) > avg + std_threshold * std:\n", + " unusual_movers.append({\n", + " 'Ticker': ticker,\n", + " 'Today % Change': round(today_change, 2),\n", + " 'Avg % Change': round(avg, 2),\n", + " 'Std Dev': round(std, 2),\n", + " 'Z-Score': round((today_change - avg)/std, 2)\n", + " })\n", + "\n", + " return pd.DataFrame(unusual_movers)\n", + "\n", + "# Example: df_extensions = ... your current DataFrame of extension alerts\n", + "# Run the screener\n", + "df_extensions = get_unusual_movers(tickers, lookback_days, std_threshold)\n", + "\n", + "# Add current UTC timestamp\n", + "df_extensions[\"Timestamp\"] = pd.Timestamp.utcnow().strftime(\"%Y-%m-%d %H:%M:%S\")\n", + "\n", + "# Ensure reports folder exists\n", + "os.makedirs(\"reports\", exist_ok=True)\n", + "\n", + "# Write to log\n", + "ext_log_file = \"reports/extension_alert_log.csv\"\n", + "df_extensions.to_csv(ext_log_file, mode=\"a\", index=False, header=not os.path.exists(ext_log_file))\n", + "\n", + "print(f\"[💾] Extension alerts logged to: {ext_log_file}\")\n", + "\n", + "# Print the dataframe\n", + "# Safe print the dataframe\n", + "if df_extensions.empty:\n", + " print(\"[✅] No unusual extension movers found today.\")\n", + "else:\n", + " print(\"[✅] Unusual Extension Movers:\")\n", + " print(df_extensions.sort_values(by='Z-Score', ascending=False).to_string(index=False))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/archive/legacy_notebooks/.ipynb_checkpoints/Zone_locate_X-checkpoint.ipynb b/archive/legacy_notebooks/.ipynb_checkpoints/Zone_locate_X-checkpoint.ipynb new file mode 100644 index 0000000..7728fa3 --- /dev/null +++ b/archive/legacy_notebooks/.ipynb_checkpoints/Zone_locate_X-checkpoint.ipynb @@ -0,0 +1,392 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[→] Checking USDCAD=X current zone...\n", + "YF.download() has changed argument auto_adjust default to True\n", + "[❌] Failed for USDCAD=X: Cannot save file into a non-existent directory: 'reports'\n", + "[→] Checking USDGBP=X current zone...\n", + "[❌] Failed for USDGBP=X: Cannot save file into a non-existent directory: 'reports'\n", + "[→] Checking USDNOK=X current zone...\n", + "[❌] Failed for USDNOK=X: Cannot save file into a non-existent directory: 'reports'\n", + "[→] Checking USDPLN=X current zone...\n", + "[❌] Failed for USDPLN=X: Cannot save file into a non-existent directory: 'reports'\n", + "[→] Checking USDAUD=X current zone...\n", + "[❌] Failed for USDAUD=X: Cannot save file into a non-existent directory: 'reports'\n", + "[→] Checking USDSGD=X current zone...\n", + "[❌] Failed for USDSGD=X: Cannot save file into a non-existent directory: 'reports'\n", + "[→] Checking USDJPY=X current zone...\n", + "[❌] Failed for USDJPY=X: Cannot save file into a non-existent directory: 'reports'\n", + "[→] Checking USDZAR=X current zone...\n", + "[❌] Failed for USDZAR=X: Cannot save file into a non-existent directory: 'reports'\n", + "[→] Checking USDBRL=X current zone...\n" + ] + }, + { + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 296\u001b[0m \u001b[1;31m#generate_alert_report()\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 297\u001b[0m \u001b[1;31m#generate_current_zone_snapshot()\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 298\u001b[1;33m \u001b[0mdf_current_zones\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mgenerate_current_zone_snapshot\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 299\u001b[0m \u001b[0mexport_current_zone_heatmap\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mdf_current_zones\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m\u001b[0m in \u001b[0;36mgenerate_current_zone_snapshot\u001b[1;34m()\u001b[0m\n\u001b[0;32m 195\u001b[0m \u001b[1;32mtry\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 196\u001b[0m \u001b[1;31m# Load latest 1H price or last daily close\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 197\u001b[1;33m \u001b[0mdata\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0myf\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdownload\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mticker\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mperiod\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;34m\"1d\"\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0minterval\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;34m\"1h\"\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mprogress\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mFalse\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 198\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mdata\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mempty\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 199\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf\"[âš ī¸] No data for {ticker}\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m~\\anaconda3\\lib\\site-packages\\yfinance\\utils.py\u001b[0m in \u001b[0;36mwrapper\u001b[1;34m(*args, **kwargs)\u001b[0m\n\u001b[0;32m 100\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 101\u001b[0m \u001b[1;32mwith\u001b[0m \u001b[0mIndentationContext\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 102\u001b[1;33m \u001b[0mresult\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mfunc\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m*\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 103\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 104\u001b[0m \u001b[0mlogger\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdebug\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf'Exiting {func.__name__}()'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m~\\anaconda3\\lib\\site-packages\\yfinance\\multi.py\u001b[0m in \u001b[0;36mdownload\u001b[1;34m(tickers, start, end, actions, threads, ignore_tz, group_by, auto_adjust, back_adjust, repair, keepna, progress, period, interval, prepost, proxy, rounding, timeout, session, multi_level_index)\u001b[0m\n\u001b[0;32m 164\u001b[0m rounding=rounding, timeout=timeout)\n\u001b[0;32m 165\u001b[0m \u001b[1;32mwhile\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mshared\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_DFS\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m<\u001b[0m \u001b[0mlen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtickers\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 166\u001b[1;33m \u001b[0m_time\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m0.01\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 167\u001b[0m \u001b[1;31m# download synchronously\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 168\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "# --- Price Level Alert System (Live Check) ---\n", + "import yfinance as yf\n", + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "from pathlib import Path\n", + "from collections import defaultdict\n", + "import pytz\n", + "import smtplib\n", + "import os\n", + "import json\n", + "\n", + "ZONE_STATE_FILE = \"reports/last_known_zone.json\"\n", + "\n", + "# Load or initialize\n", + "if os.path.exists(ZONE_STATE_FILE):\n", + " with open(ZONE_STATE_FILE, \"r\") as f:\n", + " last_known_zone = json.load(f)\n", + "else:\n", + " last_known_zone = {}\n", + "# === CONFIG ===\n", + "TICKER_LIST = [\n", + " 'USDCAD=X', 'USDGBP=X', 'USDNOK=X', 'USDPLN=X', 'USDAUD=X', 'USDSGD=X',\n", + " 'USDJPY=X', 'USDZAR=X', 'USDBRL=X', 'EURUSD=X', 'EURGBP=X', 'EURCHF=X',\n", + " 'EURPLN=X', 'EURCZK=X', 'EURNZD=X', 'EURSEK=X', 'EURZAR=X', 'EURSGD=X',\n", + " 'GBPNOK=X', 'GBPJPY=X', 'GBPAUD=X', 'GBPCAD=X', 'SEKNOK=X', 'SEKJPY=X',\n", + " 'CHFNOK=X', 'CADNOK=X', 'AUDNZD=X', 'AUDJPY=X', 'AUDSEK=X', 'AUDCAD=X',\n", + " 'NZDSGD=X', 'NZDCHF=X', 'NZDNOK=X', 'SGDJPY=X', 'SGDHKD=X', 'EURCAD=X',\n", + " 'USDCHF=X', 'GBPCHF=X', 'EURNOK=X'\n", + "]\n", + "\n", + "KEY_LEVELS_FILE = Path(r\"C:\\Users\\T460\\Documents\\Quant_trading_research\\Data Packs & Scripts\\Dev_scripts\\FX_1D\\FX_1D_KEY.xlsx\")\n", + "PIP_RANGE = 0.001\n", + "LOOKBACK_HOURS = 24\n", + "since = datetime.utcnow() - timedelta(hours=LOOKBACK_HOURS)\n", + "# === Load Key Levels ===\n", + "# === ZONE DEFINITIONS ===\n", + "ZONE_DEFINITIONS = [\n", + " (\"Premium+\", float(\"inf\"), \"Purple upper\"),\n", + " (\"Premium\", \"Purple upper\", \"Red Upper\"),\n", + " (\"Plus+\", \"Red Upper\", \"Yellow Upper\"),\n", + " (\"Fair\", \"Yellow Upper\", \"Green\"),\n", + " (\"Budget\", \"Green\", \"Yellow Lower\"),\n", + " (\"Discount\", \"Yellow Lower\", \"Red Lower\"),\n", + " (\"Clearance\", \"Red Lower\", \"Purple lower\"),\n", + " (\"Reset\", \"Purple lower\", float(\"-inf\")),\n", + "]\n", + "\n", + "def load_key_levels(filepath):\n", + " df = pd.read_excel(filepath)\n", + " df.set_index(\"Ticker\", inplace=True)\n", + " return df\n", + "\n", + "\n", + "\n", + "\n", + "# === Check if price touched a key level (with Level Name) ===\n", + "def check_proximity(level_dict, high, low):\n", + " matches = []\n", + " for level_name, level_value in level_dict.items():\n", + " if pd.isna(level_value):\n", + " continue\n", + " if (low <= level_value + PIP_RANGE) and (high >= level_value - PIP_RANGE):\n", + " matches.append({\n", + " \"Level\": round(level_value, 5),\n", + " \"Level Name\": level_name\n", + " })\n", + " return matches\n", + "\n", + "# === Summarize Touch Events ===\n", + "def summarize_touch_events(touches):\n", + " from collections import defaultdict\n", + " stats_dict = defaultdict(lambda: {\"count\": 0, \"last_touch\": None})\n", + "\n", + " for touch in touches:\n", + " key = (touch[\"Ticker\"], touch[\"Level\"], touch[\"Level Name\"])\n", + " stats_dict[key][\"count\"] += 1\n", + " stats_dict[key][\"last_touch\"] = touch[\"Time\"]\n", + "\n", + " rows = []\n", + " for (ticker, level, level_name), stats in stats_dict.items():\n", + " sast_time = stats[\"last_touch\"] + timedelta(hours=2)\n", + " rows.append({\n", + " \"Ticker\": ticker,\n", + " \"Level Name\": level_name,\n", + " \"Level\": level,\n", + " \"Touches (24h)\": stats[\"count\"],\n", + " \"Most Recent Touch (SAST)\": sast_time.strftime(\"%Y-%m-%d %H:%M\")\n", + " })\n", + "\n", + " return pd.DataFrame(rows)\n", + "\n", + "# === Main Alert Generator ===\n", + "def generate_alert_report():\n", + " global since\n", + " if 'since' not in globals():\n", + " from datetime import datetime, timedelta\n", + " LOOKBACK_HOURS = 24\n", + " since = datetime.utcnow() - timedelta(hours=LOOKBACK_HOURS)\n", + " \n", + " key_levels_df = load_key_levels(KEY_LEVELS_FILE)\n", + " touches = []\n", + "\n", + " for ticker in TICKER_LIST:\n", + " print(f\"[→] Checking {ticker}...\")\n", + " try:\n", + " # Disable yfinance progress bar → no more extra printing\n", + " data = yf.download(ticker, start=since.strftime('%Y-%m-%d'), interval=\"1h\", progress=False)\n", + " if data.empty:\n", + " print(f\"[âš ī¸] No data for {ticker}\")\n", + " continue\n", + " data = data.dropna()\n", + "\n", + " # Map ticker to short version (index row in key_levels_df)\n", + " short = ticker.split(\"=\")[0] + \"=X\" if \"=X\" in ticker else ticker\n", + "\n", + " # Prepare level dict with Level Name → Level Value\n", + " levels_series = key_levels_df.loc[short].dropna()\n", + " level_dict = dict(levels_series)\n", + "\n", + " # Process candles\n", + " for ts, row in data.iterrows():\n", + " matches = check_proximity(level_dict, row.High.item(), row.Low.item()) # <== FINAL safe version!\n", + " if matches:\n", + " for match in matches:\n", + " touches.append({\n", + " \"Ticker\": ticker,\n", + " \"Level\": match[\"Level\"],\n", + " \"Level Name\": match[\"Level Name\"],\n", + " \"Time\": ts\n", + " })\n", + "\n", + " except Exception as e:\n", + " print(f\"[❌] Failed for {ticker}: {e}\")\n", + "\n", + " if not touches:\n", + " print(\"[✅] No key levels touched in past 24 hours.\")\n", + " else:\n", + " df_summary = summarize_touch_events(touches)\n", + " df_summary = df_summary.sort_values(by=[\"Ticker\", \"Level Name\"])\n", + " print(\"[✅] Summary of Key Level Touches:\\n\")\n", + " print(df_summary.to_string(index=False))\n", + "\n", + " return\n", + "\n", + "# === Compute which zone a price is in ===\n", + "def compute_current_zone(price, zone_definitions, level_dict):\n", + " # === Prepare level name → value lookup\n", + " levels = {}\n", + " for _, upper_bound, lower_bound in zone_definitions:\n", + " if isinstance(upper_bound, str):\n", + " levels[upper_bound] = float(level_dict.get(upper_bound, float(\"inf\")))\n", + " if isinstance(lower_bound, str):\n", + " levels[lower_bound] = float(level_dict.get(lower_bound, float(\"-inf\")))\n", + "\n", + " # Force boundary defaults\n", + " levels[\"Purple upper\"] = float(level_dict.get(\"Purple upper\", float(\"inf\")))\n", + " levels[\"Purple lower\"] = float(level_dict.get(\"Purple lower\", float(\"-inf\")))\n", + "\n", + " # === Check zones\n", + " for zone_name, upper_bound, lower_bound in zone_definitions:\n", + " # Resolve boundaries\n", + " if isinstance(upper_bound, str):\n", + " upper_value = levels.get(upper_bound, float(\"inf\"))\n", + " else:\n", + " upper_value = upper_bound\n", + " if isinstance(lower_bound, str):\n", + " lower_value = levels.get(lower_bound, float(\"-inf\"))\n", + " else:\n", + " lower_value = lower_bound\n", + "\n", + " # Is price in this zone?\n", + " if lower_value < price <= upper_value:\n", + " return zone_name\n", + "\n", + " return \"Unknown\"\n", + "\n", + "def generate_current_zone_snapshot():\n", + " global since\n", + " \n", + " # === SAFETY CHECK ===\n", + " if 'ZONE_DEFINITIONS' not in globals():\n", + " print(\"[âš ī¸] ZONE_DEFINITIONS not defined — please run the ZONE_DEFINITIONS cell first.\")\n", + " return\n", + " \n", + " if 'since' not in globals():\n", + " from datetime import datetime, timedelta\n", + " LOOKBACK_HOURS = 24\n", + " since = datetime.utcnow() - timedelta(hours=LOOKBACK_HOURS)\n", + " \n", + " key_levels_df = load_key_levels(KEY_LEVELS_FILE)\n", + " current_zone_results = []\n", + "\n", + " for ticker in TICKER_LIST:\n", + " print(f\"[→] Checking {ticker} current zone...\")\n", + " try:\n", + " # Load latest 1H price or last daily close\n", + " data = yf.download(ticker, period=\"1d\", interval=\"1h\", progress=False)\n", + " if data.empty:\n", + " print(f\"[âš ī¸] No data for {ticker}\")\n", + " continue\n", + "\n", + " # === FINAL FIX → use .item() → no warning! ===\n", + " latest_close = data[\"Close\"].iloc[-1].item()\n", + " \n", + " # Map ticker to short version for your key levels\n", + " short = ticker.split(\"=\")[0] + \"=X\" if \"=X\" in ticker else ticker\n", + " \n", + " # Prepare level dict — force all floats\n", + " levels_series = key_levels_df.loc[short].dropna()\n", + " level_dict = {k: float(v) for k, v in levels_series.items()}\n", + " \n", + " # Compute zone\n", + " zone = compute_current_zone(latest_close, ZONE_DEFINITIONS, level_dict)\n", + " \n", + " # --- Update zone_history.csv ---\n", + " history_row = {\n", + " \"Date\": pd.Timestamp.utcnow().strftime(\"%Y-%m-%d\"),\n", + " \"Ticker\": ticker,\n", + " \"Zone\": zone\n", + " }\n", + " history_file = \"reports/zone_history.csv\"\n", + " pd.DataFrame([history_row]).to_csv(history_file, mode=\"a\", index=False, header=not os.path.exists(history_file))\n", + " previous_zone = last_known_zone.get(ticker, None)\n", + " if previous_zone != zone and previous_zone is not None:\n", + " transition_row = {\n", + " \"Date\": pd.Timestamp.utcnow().strftime(\"%Y-%m-%d %H:%M:%S\"),\n", + " \"Ticker\": ticker,\n", + " \"From Zone\": previous_zone,\n", + " \"To Zone\": zone\n", + " }\n", + " transition_file = \"reports/zone_transition_log.csv\"\n", + " pd.DataFrame([transition_row]).to_csv(transition_file, mode=\"a\", index=False, header=not os.path.exists(transition_file))\n", + " #update memory \n", + " last_known_zone[ticker] = zone\n", + " \n", + " print(f\"[✓] {ticker} → Zone: {zone} (Price: {latest_close:.4f})\") # Progress print\n", + " \n", + " current_zone_results.append({\n", + " \"Ticker\": ticker,\n", + " \"Current Zone\": zone,\n", + " \"Current Price\": latest_close\n", + " })\n", + " except Exception as e:\n", + " print(f\"[❌] Failed for {ticker}: {e}\")\n", + "\n", + " # Display current zones\n", + " with open(ZONE_STATE_FILE, \"w\") as f:\n", + " json.dump(last_known_zone, f)\n", + " df_current_zones = pd.DataFrame(current_zone_results)\n", + " df_current_zones = df_current_zones.sort_values(by=\"Current Zone\")\n", + " print(\"[✅] Current Zone Snapshot:\\n\")\n", + " print(df_current_zones.to_string(index=False))\n", + " \n", + " # Safe export — ensure folder exists\n", + " #import os\n", + " os.makedirs(\"reports\", exist_ok=True)\n", + "\n", + " outpath = \"reports/current_zone_snapshot.xlsx\"\n", + " df_current_zones.to_excel(outpath, index=False)\n", + " print(f\"[💾] Exported current zone snapshot to: {outpath}\")\n", + " \n", + " return df_current_zones\n", + "\n", + "def export_current_zone_heatmap(df_current_zones, output_path=\"reports/current_zone_snapshot_heatmap.xlsx\"):\n", + " if df_current_zones.empty:\n", + " print(\"[âš ī¸] No current zones to export.\")\n", + " return\n", + "\n", + " print(\"[🎨] Exporting color heatmap version...\")\n", + " with pd.ExcelWriter(output_path, engine=\"xlsxwriter\") as writer:\n", + " df_current_zones.to_excel(writer, sheet_name=\"Current Zones\", index=False)\n", + "\n", + " workbook = writer.book\n", + " worksheet = writer.sheets[\"Current Zones\"]\n", + "\n", + " # Define format rules\n", + " format_premium = workbook.add_format({\"bg_color\": \"#FFD700\", \"bold\": True}) # Gold\n", + " format_fair = workbook.add_format({\"bg_color\": \"#90EE90\"}) # LightGreen\n", + " format_budget = workbook.add_format({\"bg_color\": \"#ADD8E6\"}) # LightBlue\n", + " format_discount = workbook.add_format({\"bg_color\": \"#FF9999\"}) # LightRed\n", + "\n", + " # Apply conditional formats to Current Zone column\n", + " zone_col = df_current_zones.columns.get_loc(\"Current Zone\")\n", + " zone_range = f\"${chr(65 + zone_col)}2:${chr(65 + zone_col)}{len(df_current_zones)+1}\"\n", + "\n", + " worksheet.conditional_format(zone_range, {\"type\": \"text\", \"criteria\": \"containing\", \"value\": \"Premium\", \"format\": format_premium})\n", + " worksheet.conditional_format(zone_range, {\"type\": \"text\", \"criteria\": \"containing\", \"value\": \"Fair\", \"format\": format_fair})\n", + " worksheet.conditional_format(zone_range, {\"type\": \"text\", \"criteria\": \"containing\", \"value\": \"Budget\", \"format\": format_budget})\n", + " worksheet.conditional_format(zone_range, {\"type\": \"text\", \"criteria\": \"containing\", \"value\": \"Discount\", \"format\": format_discount})\n", + "\n", + " print(f\"[💾] Exported color heatmap to: {output_path}\")\n", + "\n", + "#generate_current_zone_snapshot()\n", + "\n", + "if __name__ == \"__main__\":\n", + " #generate_alert_report()\n", + " #generate_current_zone_snapshot()\n", + " df_current_zones = generate_current_zone_snapshot()\n", + " export_current_zone_heatmap(df_current_zones)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/archive/legacy_notebooks/.ipynb_checkpoints/Zone_transition_X-checkpoint.ipynb b/archive/legacy_notebooks/.ipynb_checkpoints/Zone_transition_X-checkpoint.ipynb new file mode 100644 index 0000000..165d77e --- /dev/null +++ b/archive/legacy_notebooks/.ipynb_checkpoints/Zone_transition_X-checkpoint.ipynb @@ -0,0 +1,357 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[→] Checking USDCAD=X...\n", + "YF.download() has changed argument auto_adjust default to True\n", + "[→] Checking USDGBP=X...\n", + "[→] Checking USDNOK=X...\n", + "[→] Checking USDPLN=X...\n", + "[→] Checking USDAUD=X...\n", + "[→] Checking USDSGD=X...\n", + "[→] Checking USDJPY=X...\n", + "[→] Checking USDZAR=X...\n", + "[→] Checking USDBRL=X...\n", + "[→] Checking EURUSD=X...\n", + "[→] Checking EURGBP=X...\n", + "[→] Checking EURCHF=X...\n", + "[→] Checking EURPLN=X...\n", + "[→] Checking EURCZK=X...\n", + "[→] Checking EURNZD=X...\n", + "[→] Checking EURSEK=X...\n", + "[→] Checking EURZAR=X...\n", + "[→] Checking EURSGD=X...\n", + "[→] Checking GBPNOK=X...\n", + "[→] Checking GBPJPY=X...\n", + "[→] Checking GBPAUD=X...\n", + "[→] Checking GBPCAD=X...\n", + "[→] Checking SEKNOK=X...\n", + "[❌] Failed for SEKNOK=X: can only concatenate str (not \"float\") to str\n", + "[→] Checking SEKJPY=X...\n", + "[→] Checking CHFNOK=X...\n", + "[→] Checking CADNOK=X...\n", + "[→] Checking AUDNZD=X...\n", + "[→] Checking AUDJPY=X...\n", + "[→] Checking AUDSEK=X...\n", + "[→] Checking AUDCAD=X...\n", + "[→] Checking NZDSGD=X...\n", + "[→] Checking NZDCHF=X...\n", + "[→] Checking NZDNOK=X...\n", + "[→] Checking SGDJPY=X...\n", + "[→] Checking SGDHKD=X...\n", + "[→] Checking EURCAD=X...\n", + "[→] Checking USDCHF=X...\n", + "[→] Checking GBPCHF=X...\n", + "[→] Checking EURNOK=X...\n", + "[✅] Summary of Key Level Touches:\n", + "\n", + " Ticker Level Name Level Touches (24h) Most Recent Touch (SAST)\n", + "AUDSEK=X Red Lower 6.2500 3 2025-07-03 08:00\n", + "EURCZK=X Yellow Upper 24.6650 6 2025-07-03 15:00\n", + "EURGBP=X Yellow Upper 0.8616 29 2025-07-04 21:00\n", + "EURNZD=X Purple upper 1.9415 24 2025-07-04 15:00\n", + "EURPLN=X Red Lower 4.2475 8 2025-07-04 14:00\n", + "EURZAR=X Red Upper 20.6880 7 2025-07-04 12:00\n", + "NZDNOK=X Yellow Lower 6.1300 1 2025-07-03 01:00\n", + "SEKJPY=X Purple upper 15.1650 1 2025-07-03 16:00\n", + "USDBRL=X Green 5.4065 10 2025-07-04 18:00\n", + "USDNOK=X Green 10.0550 17 2025-07-04 16:00\n", + "USDZAR=X Yellow Upper 17.6300 5 2025-07-04 18:00\n", + "[→] Preparing current prices and zones for hits...\n" + ] + } + ], + "source": [ + "import yfinance as yf\n", + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "from pathlib import Path\n", + "from collections import defaultdict\n", + "import pytz\n", + "import smtplib\n", + "import os\n", + "\n", + "ZONE_DEFINITIONS = [\n", + " (\"Premium+\", float(\"inf\"), \"Purple upper\"),\n", + " (\"Premium\", \"Purple upper\", \"Red Upper\"),\n", + " (\"Plus+\", \"Red Upper\", \"Yellow Upper\"),\n", + " (\"Fair\", \"Yellow Upper\", \"Green\"),\n", + " (\"Budget\", \"Green\", \"Yellow Lower\"),\n", + " (\"Discount\", \"Yellow Lower\", \"Red Lower\"),\n", + " (\"Clearance\", \"Red Lower\", \"Purple lower\"),\n", + " (\"Reset\", \"Purple lower\", float(\"-inf\")),\n", + "]\n", + "\n", + "# === Current Zone Computation ===\n", + "def compute_current_zone(price, zone_definitions, level_dict):\n", + " # Prepare level name → value lookup\n", + " levels = {}\n", + " for zone_name, a, b in zone_definitions:\n", + " if isinstance(a, str):\n", + " levels[a] = level_dict.get(a, None)\n", + " if isinstance(b, str):\n", + " levels[b] = level_dict.get(b, None)\n", + " levels[\"Purple upper\"] = level_dict.get(\"Purple upper\", float(\"inf\"))\n", + " levels[\"Purple lower\"] = level_dict.get(\"Purple lower\", float(\"-inf\"))\n", + " \n", + " for zone_name, upper_bound, lower_bound in zone_definitions:\n", + " # Resolve upper/lower boundaries\n", + " if isinstance(upper_bound, str):\n", + " upper_value = levels.get(upper_bound, float(\"inf\"))\n", + " else:\n", + " upper_value = upper_bound\n", + " if isinstance(lower_bound, str):\n", + " lower_value = levels.get(lower_bound, float(\"-inf\"))\n", + " else:\n", + " lower_value = lower_bound\n", + " \n", + " # Is price in this zone?\n", + " if lower_value < price <= upper_value:\n", + " return zone_name\n", + " \n", + " return \"Unknown\"\n", + "\n", + "\n", + "# === CONFIG ===\n", + "TICKER_LIST = [\n", + " 'USDCAD=X', 'USDGBP=X', 'USDNOK=X', 'USDPLN=X', 'USDAUD=X', 'USDSGD=X',\n", + " 'USDJPY=X', 'USDZAR=X', 'USDBRL=X', 'EURUSD=X', 'EURGBP=X', 'EURCHF=X',\n", + " 'EURPLN=X', 'EURCZK=X', 'EURNZD=X', 'EURSEK=X', 'EURZAR=X', 'EURSGD=X',\n", + " 'GBPNOK=X', 'GBPJPY=X', 'GBPAUD=X', 'GBPCAD=X', 'SEKNOK=X', 'SEKJPY=X',\n", + " 'CHFNOK=X', 'CADNOK=X', 'AUDNZD=X', 'AUDJPY=X', 'AUDSEK=X', 'AUDCAD=X',\n", + " 'NZDSGD=X', 'NZDCHF=X', 'NZDNOK=X', 'SGDJPY=X', 'SGDHKD=X', 'EURCAD=X',\n", + " 'USDCHF=X', 'GBPCHF=X', 'EURNOK=X'\n", + "]\n", + "\n", + "\n", + "KEY_LEVELS_FILE = Path(r\"C:\\Users\\T460\\Documents\\Quant_trading_research\\Data Packs & Scripts\\Dev_scripts\\FX_1D\\FX_1D_KEY.xlsx\")\n", + "PIP_RANGE = 0.001\n", + "LOOKBACK_HOURS = 24\n", + "since = datetime.utcnow() - timedelta(hours=LOOKBACK_HOURS)\n", + "# === Load Key Levels ===\n", + "def load_key_levels(filepath):\n", + " df = pd.read_excel(filepath)\n", + " df.set_index(\"Ticker\", inplace=True)\n", + " return df\n", + "\n", + "# === Check if price touched a key level (with Level Name) ===\n", + "def check_proximity(level_dict, high, low):\n", + " matches = []\n", + " for level_name, level_value in level_dict.items():\n", + " if pd.isna(level_value):\n", + " continue\n", + " if (low <= level_value + PIP_RANGE) and (high >= level_value - PIP_RANGE):\n", + " matches.append({\n", + " \"Level\": round(level_value, 5),\n", + " \"Level Name\": level_name\n", + " })\n", + " return matches\n", + "\n", + "# === Summarize Touch Events ===\n", + "def summarize_touch_events(touches):\n", + " from collections import defaultdict\n", + " stats_dict = defaultdict(lambda: {\"count\": 0, \"last_touch\": None})\n", + "\n", + " for touch in touches:\n", + " key = (touch[\"Ticker\"], touch[\"Level\"], touch[\"Level Name\"])\n", + " stats_dict[key][\"count\"] += 1\n", + " stats_dict[key][\"last_touch\"] = touch[\"Time\"]\n", + "\n", + " rows = []\n", + " for (ticker, level, level_name), stats in stats_dict.items():\n", + " sast_time = stats[\"last_touch\"] + timedelta(hours=2)\n", + " rows.append({\n", + " \"Ticker\": ticker,\n", + " \"Level Name\": level_name,\n", + " \"Level\": level,\n", + " \"Touches (24h)\": stats[\"count\"],\n", + " \"Most Recent Touch (SAST)\": sast_time.strftime(\"%Y-%m-%d %H:%M\")\n", + " })\n", + "\n", + " return pd.DataFrame(rows)\n", + "\n", + "# === Main Alert Generator ===\n", + "def generate_alert_report():\n", + " global since\n", + " if 'since' not in globals():\n", + " LOOKBACK_HOURS = 24\n", + " since = datetime.utcnow() - timedelta(hours=LOOKBACK_HOURS)\n", + " \n", + " key_levels_df = load_key_levels(KEY_LEVELS_FILE)\n", + " touches = []\n", + "\n", + " for ticker in TICKER_LIST:\n", + " print(f\"[→] Checking {ticker}...\")\n", + "\n", + " try:\n", + " if datetime.utcnow().weekday() >= 5:\n", + " print(f\"[â„šī¸] Skipping {ticker} → Weekend\")\n", + " continue\n", + "\n", + " data = yf.download(ticker, start=since.strftime('%Y-%m-%d'), interval=\"1h\", progress=False)\n", + " if data.empty:\n", + " print(f\"[âš ī¸] No data for {ticker} — likely weekend or market closed.\")\n", + " continue\n", + " data = data.dropna()\n", + "\n", + " short = ticker.split(\"=\")[0] + \"=X\" if \"=X\" in ticker else ticker\n", + " levels_series = key_levels_df.loc[short].dropna()\n", + " level_dict = dict(levels_series)\n", + "\n", + " for ts, row in data.iterrows():\n", + " matches = check_proximity(level_dict, row.High.item(), row.Low.item())\n", + " if matches:\n", + " for match in matches:\n", + " touches.append({\n", + " \"Ticker\": ticker,\n", + " \"Level\": match[\"Level\"],\n", + " \"Level Name\": match[\"Level Name\"],\n", + " \"Time\": ts\n", + " })\n", + "\n", + " except Exception as e:\n", + " print(f\"[❌] Failed for {ticker}: {e}\")\n", + "\n", + " if not touches:\n", + " print(\"[✅] No key levels touched in past 24 hours.\")\n", + " alert_df = pd.DataFrame() # empty df if no touches\n", + " else:\n", + " alert_df = summarize_touch_events(touches)\n", + " alert_df = alert_df.sort_values(by=[\"Ticker\", \"Level Name\"])\n", + " print(\"[✅] Summary of Key Level Touches:\\n\")\n", + " print(alert_df.to_string(index=False))\n", + "\n", + " # Return BOTH alert_df and key_levels_df\n", + " return alert_df, key_levels_df\n", + "\n", + "# === PHASE 1B → Key Level Hit Log with From Zone ===\n", + "\n", + "# === FINAL V2 SAFE PATCH → log_key_level_hits() ===\n", + "def log_key_level_hits(alert_df, key_levels_df):\n", + " if alert_df.empty:\n", + " print(\"[âš ī¸] No key level hits to log.\")\n", + " return\n", + " \n", + " # Load current prices → to compute current zone\n", + " print(\"[→] Preparing current prices and zones for hits...\")\n", + " \n", + " os.makedirs(\"reports\", exist_ok=True)\n", + " \n", + " log_records = []\n", + " for _, row in alert_df.iterrows():\n", + " ticker = row[\"Ticker\"]\n", + " level_name = row[\"Level Name\"]\n", + " level_value = row[\"Level\"]\n", + " touch_time = row[\"Most Recent Touch (SAST)\"]\n", + "\n", + " try:\n", + " # Try 1h first → fallback to daily\n", + " data = yf.download(ticker, period=\"1d\", interval=\"1h\", progress=False)\n", + " if data.empty:\n", + " print(f\"[âš ī¸] No 1H data for {ticker} → trying Daily...\")\n", + " data = yf.download(ticker, period=\"5d\", interval=\"1d\", progress=False)\n", + "\n", + " if data.empty:\n", + " print(f\"[â„šī¸] Skipping {ticker} → Market likely closed (no data)\")\n", + " continue\n", + " \n", + " data = data.dropna()\n", + "\n", + " current_price = data[\"Close\"].iloc[-1].item()\n", + "\n", + " # Prepare level dict\n", + " short = ticker.split(\"=\")[0] + \"=X\" if \"=X\" in ticker else ticker\n", + " levels_series = key_levels_df.loc[short].dropna()\n", + " level_dict = dict(levels_series)\n", + "\n", + " # Current Zone\n", + " current_zone = compute_current_zone(current_price, ZONE_DEFINITIONS, level_dict)\n", + "\n", + " # From Zone → assume level_value is approximate price at touch\n", + " from_zone = compute_current_zone(level_value, ZONE_DEFINITIONS, level_dict)\n", + "\n", + " log_records.append({\n", + " \"Timestamp\": pd.Timestamp.utcnow().strftime(\"%Y-%m-%d %H:%M:%S\"),\n", + " \"Ticker\": ticker,\n", + " \"Level Name\": level_name,\n", + " \"Level\": level_value,\n", + " \"Touch Time (UTC)\": pd.to_datetime(touch_time).tz_localize('Africa/Johannesburg').tz_convert('UTC').strftime(\"%Y-%m-%d %H:%M:%S\"),\n", + " \"From Zone\": from_zone,\n", + " \"Current Zone\": current_zone,\n", + " \"Current Price\": current_price\n", + " })\n", + " \n", + " except Exception as e:\n", + " print(f\"[❌] Failed for {ticker}: {e}\")\n", + "\n", + " df_log = pd.DataFrame(log_records)\n", + " \n", + " # Append to CSV log\n", + " log_file = \"reports/key_level_hit_log.csv\"\n", + " df_log.to_csv(log_file, mode=\"a\", index=False, header=not os.path.exists(log_file))\n", + " \n", + " print(f\"[💾] Key Level Hit Log saved to: {log_file}\")\n", + " print(df_log.to_string(index=False))\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " alert_df, key_levels_df = generate_alert_report()\n", + " log_key_level_hits(alert_df, key_levels_df)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(ZONE_DEFINITIONS)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/archive/legacy_notebooks/Top_movers_X.ipynb b/archive/legacy_notebooks/Top_movers_X.ipynb new file mode 100644 index 0000000..fcbf303 --- /dev/null +++ b/archive/legacy_notebooks/Top_movers_X.ipynb @@ -0,0 +1,200 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "YF.download() has changed argument auto_adjust default to True\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed\n", + "[*********************100%***********************] 1 of 1 completed" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[💾] Extension alerts logged to: reports/extension_alert_log.csv\n", + "[✅] Unusual Extension Movers:\n", + " Ticker Today % Change Avg % Change Std Dev Z-Score Timestamp\n", + "USDPLN=X 0.36 -0.37 0.29 2.58 2025-07-04 19:02:55\n", + "EURGBP=X 0.66 0.04 0.26 2.37 2025-07-04 19:02:55\n", + "USDJPY=X 0.66 -0.10 0.38 2.02 2025-07-04 19:02:55\n", + "SGDJPY=X 0.58 -0.00 0.30 1.92 2025-07-04 19:02:55\n", + "USDCHF=X 0.10 -0.39 0.32 1.55 2025-07-04 19:02:55\n", + "GBPCHF=X -0.58 -0.12 0.31 -1.48 2025-07-04 19:02:55\n", + "NZDSGD=X -0.47 0.00 0.30 -1.60 2025-07-04 19:02:55\n", + "EURPLN=X -0.42 -0.03 0.22 -1.81 2025-07-04 19:02:55\n", + "GBPAUD=X -0.73 0.15 0.37 -2.39 2025-07-04 19:02:55\n", + "GBPCAD=X -1.11 0.23 0.40 -3.40 2025-07-04 19:02:55\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "import yfinance as yf\n", + "import pandas as pd\n", + "import os\n", + "\n", + "\n", + "# Parameters\n", + "tickers = [\n", + " 'USDCAD=X', 'USDGBP=X', 'USDNOK=X', 'USDPLN=X', 'USDAUD=X', 'USDSGD=X',\n", + " 'USDJPY=X', 'USDZAR=X', 'USDBRL=X', 'EURUSD=X', 'EURGBP=X', 'EURCHF=X',\n", + " 'EURPLN=X', 'EURCZK=X', 'EURNZD=X', 'EURSEK=X', 'EURZAR=X', 'EURSGD=X',\n", + " 'GBPNOK=X', 'GBPJPY=X', 'GBPAUD=X', 'GBPCAD=X', 'SEKNOK=X', 'SEKJPY=X',\n", + " 'CHFNOK=X', 'CADNOK=X', 'AUDNZD=X', 'AUDJPY=X', 'AUDSEK=X', 'AUDCAD=X',\n", + " 'NZDSGD=X', 'NZDCHF=X', 'NZDNOK=X', 'SGDJPY=X', 'SGDHKD=X', 'EURCAD=X',\n", + " 'USDCHF=X', 'GBPCHF=X', 'EURNOK=X'\n", + "] # Add more tickers\n", + "lookback_days = 10\n", + "std_threshold = 1.5 # Flag moves above 1.5x std deviation\n", + "\n", + "def get_unusual_movers(tickers, lookback_days, std_threshold):\n", + " unusual_movers = []\n", + "\n", + " for ticker in tickers:\n", + " data = yf.download(ticker, period=f\"{lookback_days + 2}d\", interval='1d')\n", + " data['Pct Change'] = data['Close'].pct_change() * 100\n", + " \n", + " recent_changes = data['Pct Change'].iloc[-(lookback_days+1):-1] # Exclude today\n", + " today_change = data['Pct Change'].iloc[-1]\n", + "\n", + " avg = recent_changes.mean()\n", + " std = recent_changes.std()\n", + "\n", + " if abs(today_change) > avg + std_threshold * std:\n", + " unusual_movers.append({\n", + " 'Ticker': ticker,\n", + " 'Today % Change': round(today_change, 2),\n", + " 'Avg % Change': round(avg, 2),\n", + " 'Std Dev': round(std, 2),\n", + " 'Z-Score': round((today_change - avg)/std, 2)\n", + " })\n", + "\n", + " return pd.DataFrame(unusual_movers)\n", + "\n", + "# Example: df_extensions = ... your current DataFrame of extension alerts\n", + "# Run the screener\n", + "df_extensions = get_unusual_movers(tickers, lookback_days, std_threshold)\n", + "\n", + "# Add current UTC timestamp\n", + "df_extensions[\"Timestamp\"] = pd.Timestamp.utcnow().strftime(\"%Y-%m-%d %H:%M:%S\")\n", + "\n", + "# Ensure reports folder exists\n", + "os.makedirs(\"reports\", exist_ok=True)\n", + "\n", + "# Write to log\n", + "ext_log_file = \"reports/extension_alert_log.csv\"\n", + "df_extensions.to_csv(ext_log_file, mode=\"a\", index=False, header=not os.path.exists(ext_log_file))\n", + "\n", + "print(f\"[💾] Extension alerts logged to: {ext_log_file}\")\n", + "\n", + "# Print the dataframe\n", + "# Safe print the dataframe\n", + "if df_extensions.empty:\n", + " print(\"[✅] No unusual extension movers found today.\")\n", + "else:\n", + " print(\"[✅] Unusual Extension Movers:\")\n", + " print(df_extensions.sort_values(by='Z-Score', ascending=False).to_string(index=False))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/archive/legacy_notebooks/Zone_locate_X.ipynb b/archive/legacy_notebooks/Zone_locate_X.ipynb new file mode 100644 index 0000000..cb45780 --- /dev/null +++ b/archive/legacy_notebooks/Zone_locate_X.ipynb @@ -0,0 +1,483 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[→] Checking USDCAD=X current zone...\n", + "YF.download() has changed argument auto_adjust default to True\n", + "[✓] USDCAD=X → Zone: Fair (Price: 1.3603)\n", + "[→] Checking USDGBP=X current zone...\n", + "[✓] USDGBP=X → Zone: Clearance (Price: 0.7323)\n", + "[→] Checking USDNOK=X current zone...\n", + "[✓] USDNOK=X → Zone: Fair (Price: 10.0688)\n", + "[→] Checking USDPLN=X current zone...\n", + "[✓] USDPLN=X → Zone: Reset (Price: 3.6009)\n", + "[→] Checking USDAUD=X current zone...\n", + "[✓] USDAUD=X → Zone: Plus+ (Price: 1.5253)\n", + "[→] Checking USDSGD=X current zone...\n", + "[✓] USDSGD=X → Zone: Reset (Price: 1.2740)\n", + "[→] Checking USDJPY=X current zone...\n", + "[✓] USDJPY=X → Zone: Budget (Price: 144.4760)\n", + "[→] Checking USDZAR=X current zone...\n", + "[✓] USDZAR=X → Zone: Fair (Price: 17.6117)\n", + "[→] Checking USDBRL=X current zone...\n", + "[✓] USDBRL=X → Zone: Fair (Price: 5.4232)\n", + "[→] Checking EURUSD=X current zone...\n", + "[✓] EURUSD=X → Zone: Plus+ (Price: 1.1783)\n", + "[→] Checking EURGBP=X current zone...\n", + "[✓] EURGBP=X → Zone: Plus+ (Price: 0.8626)\n", + "[→] Checking EURCHF=X current zone...\n", + "[✓] EURCHF=X → Zone: Clearance (Price: 0.9349)\n", + "[→] Checking EURPLN=X current zone...\n", + "[✓] EURPLN=X → Zone: Clearance (Price: 4.2418)\n", + "[→] Checking EURCZK=X current zone...\n", + "[✓] EURCZK=X → Zone: Fair (Price: 24.6333)\n", + "[→] Checking EURNZD=X current zone...\n", + "[✓] EURNZD=X → Zone: Premium+ (Price: 1.9447)\n", + "[→] Checking EURSEK=X current zone...\n", + "[✓] EURSEK=X → Zone: Fair (Price: 11.2569)\n", + "[→] Checking EURZAR=X current zone...\n", + "[✓] EURZAR=X → Zone: Premium (Price: 20.7443)\n", + "[→] Checking EURSGD=X current zone...\n", + "[✓] EURSGD=X → Zone: Plus+ (Price: 1.5004)\n", + "[→] Checking GBPNOK=X current zone...\n", + "[✓] GBPNOK=X → Zone: Plus+ (Price: 13.7491)\n", + "[→] Checking GBPJPY=X current zone...\n", + "[✓] GBPJPY=X → Zone: Premium (Price: 197.2540)\n", + "[→] Checking GBPAUD=X current zone...\n", + "[✓] GBPAUD=X → Zone: Premium (Price: 2.0844)\n", + "[→] Checking GBPCAD=X current zone...\n", + "[✓] GBPCAD=X → Zone: Premium (Price: 1.8571)\n", + "[→] Checking SEKNOK=X current zone...\n", + "[❌] Failed for SEKNOK=X: could not convert string to float: ''\n", + "[→] Checking SEKJPY=X current zone...\n", + "[✓] SEKJPY=X → Zone: Premium (Price: 15.1110)\n", + "[→] Checking CHFNOK=X current zone...\n", + "[✓] CHFNOK=X → Zone: Premium (Price: 12.6864)\n", + "[→] Checking CADNOK=X current zone...\n", + "[✓] CADNOK=X → Zone: Budget (Price: 7.4022)\n", + "[→] Checking AUDNZD=X current zone...\n", + "[✓] AUDNZD=X → Zone: Fair (Price: 1.0812)\n", + "[→] Checking AUDJPY=X current zone...\n", + "[✓] AUDJPY=X → Zone: Fair (Price: 94.6220)\n", + "[→] Checking AUDSEK=X current zone...\n", + "[✓] AUDSEK=X → Zone: Discount (Price: 6.2574)\n", + "[→] Checking AUDCAD=X current zone...\n", + "[✓] AUDCAD=X → Zone: Budget (Price: 0.8907)\n", + "[→] Checking NZDSGD=X current zone...\n", + "[✓] NZDSGD=X → Zone: Clearance (Price: 0.7712)\n", + "[→] Checking NZDCHF=X current zone...\n", + "[✓] NZDCHF=X → Zone: Clearance (Price: 0.4807)\n", + "[→] Checking NZDNOK=X current zone...\n", + "[✓] NZDNOK=X → Zone: Discount (Price: 6.0972)\n", + "[→] Checking SGDJPY=X current zone...\n", + "[✓] SGDJPY=X → Zone: Premium (Price: 113.3820)\n", + "[→] Checking SGDHKD=X current zone...\n", + "[✓] SGDHKD=X → Zone: Premium+ (Price: 6.1597)\n", + "[→] Checking EURCAD=X current zone...\n", + "[✓] EURCAD=X → Zone: Premium+ (Price: 1.6021)\n", + "[→] Checking USDCHF=X current zone...\n", + "[✓] USDCHF=X → Zone: Reset (Price: 0.7937)\n", + "[→] Checking GBPCHF=X current zone...\n", + "[✓] GBPCHF=X → Zone: Clearance (Price: 1.0837)\n", + "[→] Checking EURNOK=X current zone...\n", + "[✓] EURNOK=X → Zone: Premium (Price: 11.8600)\n", + "[✅] Current Zone Snapshot:\n", + "\n", + " Ticker Current Zone Current Price\n", + "AUDCAD=X Budget 0.890720\n", + "CADNOK=X Budget 7.402200\n", + "USDJPY=X Budget 144.475998\n", + "EURPLN=X Clearance 4.241760\n", + "EURCHF=X Clearance 0.934920\n", + "GBPCHF=X Clearance 1.083670\n", + "NZDCHF=X Clearance 0.480700\n", + "USDGBP=X Clearance 0.732350\n", + "NZDSGD=X Clearance 0.771200\n", + "NZDNOK=X Discount 6.097200\n", + "AUDSEK=X Discount 6.257400\n", + "USDZAR=X Fair 17.611750\n", + "USDBRL=X Fair 5.423200\n", + "USDNOK=X Fair 10.068760\n", + "EURCZK=X Fair 24.633301\n", + "AUDJPY=X Fair 94.622002\n", + "EURSEK=X Fair 11.256920\n", + "AUDNZD=X Fair 1.081150\n", + "USDCAD=X Fair 1.360280\n", + "GBPNOK=X Plus+ 13.749130\n", + "EURUSD=X Plus+ 1.178273\n", + "USDAUD=X Plus+ 1.525260\n", + "EURSGD=X Plus+ 1.500430\n", + "EURGBP=X Plus+ 0.862630\n", + "SGDJPY=X Premium 113.382004\n", + "GBPJPY=X Premium 197.253998\n", + "GBPAUD=X Premium 2.084370\n", + "EURZAR=X Premium 20.744329\n", + "CHFNOK=X Premium 12.686400\n", + "SEKJPY=X Premium 15.111000\n", + "GBPCAD=X Premium 1.857060\n", + "EURNOK=X Premium 11.860000\n", + "SGDHKD=X Premium+ 6.159700\n", + "EURCAD=X Premium+ 1.602060\n", + "EURNZD=X Premium+ 1.944710\n", + "USDSGD=X Reset 1.273960\n", + "USDPLN=X Reset 3.600900\n", + "USDCHF=X Reset 0.793710\n", + "[💾] Exported current zone snapshot to: reports/current_zone_snapshot.xlsx\n", + "[🎨] Exporting color heatmap version...\n", + "[💾] Exported color heatmap to: reports/current_zone_snapshot_heatmap.xlsx\n" + ] + } + ], + "source": [ + "# --- Price Level Alert System (Live Check) ---\n", + "import yfinance as yf\n", + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "from pathlib import Path\n", + "from collections import defaultdict\n", + "import pytz\n", + "import smtplib\n", + "import os\n", + "import json\n", + "\n", + "ZONE_STATE_FILE = \"reports/last_known_zone.json\"\n", + "\n", + "# Load or initialize\n", + "if os.path.exists(ZONE_STATE_FILE):\n", + " with open(ZONE_STATE_FILE, \"r\") as f:\n", + " last_known_zone = json.load(f)\n", + "else:\n", + " last_known_zone = {}\n", + "# === CONFIG ===\n", + "TICKER_LIST = [\n", + " 'USDCAD=X', 'USDGBP=X', 'USDNOK=X', 'USDPLN=X', 'USDAUD=X', 'USDSGD=X',\n", + " 'USDJPY=X', 'USDZAR=X', 'USDBRL=X', 'EURUSD=X', 'EURGBP=X', 'EURCHF=X',\n", + " 'EURPLN=X', 'EURCZK=X', 'EURNZD=X', 'EURSEK=X', 'EURZAR=X', 'EURSGD=X',\n", + " 'GBPNOK=X', 'GBPJPY=X', 'GBPAUD=X', 'GBPCAD=X', 'SEKNOK=X', 'SEKJPY=X',\n", + " 'CHFNOK=X', 'CADNOK=X', 'AUDNZD=X', 'AUDJPY=X', 'AUDSEK=X', 'AUDCAD=X',\n", + " 'NZDSGD=X', 'NZDCHF=X', 'NZDNOK=X', 'SGDJPY=X', 'SGDHKD=X', 'EURCAD=X',\n", + " 'USDCHF=X', 'GBPCHF=X', 'EURNOK=X'\n", + "]\n", + "\n", + "KEY_LEVELS_FILE = Path(r\"C:\\Users\\T460\\Documents\\Quant_trading_research\\Data Packs & Scripts\\Dev_scripts\\FX_1D\\FX_1D_KEY.xlsx\")\n", + "PIP_RANGE = 0.001\n", + "LOOKBACK_HOURS = 24\n", + "since = datetime.utcnow() - timedelta(hours=LOOKBACK_HOURS)\n", + "# === Load Key Levels ===\n", + "# === ZONE DEFINITIONS ===\n", + "ZONE_DEFINITIONS = [\n", + " (\"Premium+\", float(\"inf\"), \"Purple upper\"),\n", + " (\"Premium\", \"Purple upper\", \"Red Upper\"),\n", + " (\"Plus+\", \"Red Upper\", \"Yellow Upper\"),\n", + " (\"Fair\", \"Yellow Upper\", \"Green\"),\n", + " (\"Budget\", \"Green\", \"Yellow Lower\"),\n", + " (\"Discount\", \"Yellow Lower\", \"Red Lower\"),\n", + " (\"Clearance\", \"Red Lower\", \"Purple lower\"),\n", + " (\"Reset\", \"Purple lower\", float(\"-inf\")),\n", + "]\n", + "\n", + "def load_key_levels(filepath):\n", + " df = pd.read_excel(filepath)\n", + " df.set_index(\"Ticker\", inplace=True)\n", + " return df\n", + "\n", + "\n", + "\n", + "\n", + "# === Check if price touched a key level (with Level Name) ===\n", + "def check_proximity(level_dict, high, low):\n", + " matches = []\n", + " for level_name, level_value in level_dict.items():\n", + " if pd.isna(level_value):\n", + " continue\n", + " if (low <= level_value + PIP_RANGE) and (high >= level_value - PIP_RANGE):\n", + " matches.append({\n", + " \"Level\": round(level_value, 5),\n", + " \"Level Name\": level_name\n", + " })\n", + " return matches\n", + "\n", + "# === Summarize Touch Events ===\n", + "def summarize_touch_events(touches):\n", + " from collections import defaultdict\n", + " stats_dict = defaultdict(lambda: {\"count\": 0, \"last_touch\": None})\n", + "\n", + " for touch in touches:\n", + " key = (touch[\"Ticker\"], touch[\"Level\"], touch[\"Level Name\"])\n", + " stats_dict[key][\"count\"] += 1\n", + " stats_dict[key][\"last_touch\"] = touch[\"Time\"]\n", + "\n", + " rows = []\n", + " for (ticker, level, level_name), stats in stats_dict.items():\n", + " sast_time = stats[\"last_touch\"] + timedelta(hours=2)\n", + " rows.append({\n", + " \"Ticker\": ticker,\n", + " \"Level Name\": level_name,\n", + " \"Level\": level,\n", + " \"Touches (24h)\": stats[\"count\"],\n", + " \"Most Recent Touch (SAST)\": sast_time.strftime(\"%Y-%m-%d %H:%M\")\n", + " })\n", + "\n", + " return pd.DataFrame(rows)\n", + "\n", + "# === Main Alert Generator ===\n", + "def generate_alert_report():\n", + " global since\n", + " if 'since' not in globals():\n", + " from datetime import datetime, timedelta\n", + " LOOKBACK_HOURS = 24\n", + " since = datetime.utcnow() - timedelta(hours=LOOKBACK_HOURS)\n", + " \n", + " key_levels_df = load_key_levels(KEY_LEVELS_FILE)\n", + " touches = []\n", + "\n", + " for ticker in TICKER_LIST:\n", + " print(f\"[→] Checking {ticker}...\")\n", + " try:\n", + " # Disable yfinance progress bar → no more extra printing\n", + " data = yf.download(ticker, start=since.strftime('%Y-%m-%d'), interval=\"1h\", progress=False)\n", + " if data.empty:\n", + " print(f\"[âš ī¸] No data for {ticker}\")\n", + " continue\n", + " data = data.dropna()\n", + "\n", + " # Map ticker to short version (index row in key_levels_df)\n", + " short = ticker.split(\"=\")[0] + \"=X\" if \"=X\" in ticker else ticker\n", + "\n", + " # Prepare level dict with Level Name → Level Value\n", + " levels_series = key_levels_df.loc[short].dropna()\n", + " level_dict = dict(levels_series)\n", + "\n", + " # Process candles\n", + " for ts, row in data.iterrows():\n", + " matches = check_proximity(level_dict, row.High.item(), row.Low.item()) # <== FINAL safe version!\n", + " if matches:\n", + " for match in matches:\n", + " touches.append({\n", + " \"Ticker\": ticker,\n", + " \"Level\": match[\"Level\"],\n", + " \"Level Name\": match[\"Level Name\"],\n", + " \"Time\": ts\n", + " })\n", + "\n", + " except Exception as e:\n", + " print(f\"[❌] Failed for {ticker}: {e}\")\n", + "\n", + " if not touches:\n", + " print(\"[✅] No key levels touched in past 24 hours.\")\n", + " else:\n", + " df_summary = summarize_touch_events(touches)\n", + " df_summary = df_summary.sort_values(by=[\"Ticker\", \"Level Name\"])\n", + " print(\"[✅] Summary of Key Level Touches:\\n\")\n", + " print(df_summary.to_string(index=False))\n", + "\n", + " return\n", + "\n", + "# === Compute which zone a price is in ===\n", + "def compute_current_zone(price, zone_definitions, level_dict):\n", + " # === Prepare level name → value lookup\n", + " levels = {}\n", + " for _, upper_bound, lower_bound in zone_definitions:\n", + " if isinstance(upper_bound, str):\n", + " levels[upper_bound] = float(level_dict.get(upper_bound, float(\"inf\")))\n", + " if isinstance(lower_bound, str):\n", + " levels[lower_bound] = float(level_dict.get(lower_bound, float(\"-inf\")))\n", + "\n", + " # Force boundary defaults\n", + " levels[\"Purple upper\"] = float(level_dict.get(\"Purple upper\", float(\"inf\")))\n", + " levels[\"Purple lower\"] = float(level_dict.get(\"Purple lower\", float(\"-inf\")))\n", + "\n", + " # === Check zones\n", + " for zone_name, upper_bound, lower_bound in zone_definitions:\n", + " # Resolve boundaries\n", + " if isinstance(upper_bound, str):\n", + " upper_value = levels.get(upper_bound, float(\"inf\"))\n", + " else:\n", + " upper_value = upper_bound\n", + " if isinstance(lower_bound, str):\n", + " lower_value = levels.get(lower_bound, float(\"-inf\"))\n", + " else:\n", + " lower_value = lower_bound\n", + "\n", + " # Is price in this zone?\n", + " if lower_value < price <= upper_value:\n", + " return zone_name\n", + "\n", + " return \"Unknown\"\n", + "\n", + "def generate_current_zone_snapshot():\n", + " global since\n", + " \n", + " # === SAFETY CHECK ===\n", + " if 'ZONE_DEFINITIONS' not in globals():\n", + " print(\"[âš ī¸] ZONE_DEFINITIONS not defined — please run the ZONE_DEFINITIONS cell first.\")\n", + " return\n", + " \n", + " if 'since' not in globals():\n", + " from datetime import datetime, timedelta\n", + " LOOKBACK_HOURS = 24\n", + " since = datetime.utcnow() - timedelta(hours=LOOKBACK_HOURS)\n", + " \n", + " key_levels_df = load_key_levels(KEY_LEVELS_FILE)\n", + " current_zone_results = []\n", + "\n", + " for ticker in TICKER_LIST:\n", + " print(f\"[→] Checking {ticker} current zone...\")\n", + " try:\n", + " # Load latest 1H price or last daily close\n", + " data = yf.download(ticker, period=\"1d\", interval=\"1h\", progress=False)\n", + " if data.empty:\n", + " print(f\"[âš ī¸] No data for {ticker}\")\n", + " continue\n", + "\n", + " # === FINAL FIX → use .item() → no warning! ===\n", + " latest_close = data[\"Close\"].iloc[-1].item()\n", + " \n", + " # Map ticker to short version for your key levels\n", + " short = ticker.split(\"=\")[0] + \"=X\" if \"=X\" in ticker else ticker\n", + " \n", + " # Prepare level dict — force all floats\n", + " levels_series = key_levels_df.loc[short].dropna()\n", + " level_dict = {k: float(v) for k, v in levels_series.items()}\n", + " \n", + " # Compute zone\n", + " zone = compute_current_zone(latest_close, ZONE_DEFINITIONS, level_dict)\n", + " \n", + " # --- Update zone_history.csv ---\n", + " history_row = {\n", + " \"Date\": pd.Timestamp.utcnow().strftime(\"%Y-%m-%d\"),\n", + " \"Ticker\": ticker,\n", + " \"Zone\": zone\n", + " }\n", + " history_file = \"reports/zone_history.csv\"\n", + " pd.DataFrame([history_row]).to_csv(history_file, mode=\"a\", index=False, header=not os.path.exists(history_file))\n", + " previous_zone = last_known_zone.get(ticker, None)\n", + " if previous_zone != zone and previous_zone is not None:\n", + " transition_row = {\n", + " \"Date\": pd.Timestamp.utcnow().strftime(\"%Y-%m-%d %H:%M:%S\"),\n", + " \"Ticker\": ticker,\n", + " \"From Zone\": previous_zone,\n", + " \"To Zone\": zone\n", + " }\n", + " transition_file = \"reports/zone_transition_log.csv\"\n", + " pd.DataFrame([transition_row]).to_csv(transition_file, mode=\"a\", index=False, header=not os.path.exists(transition_file))\n", + " #update memory \n", + " last_known_zone[ticker] = zone\n", + " \n", + " print(f\"[✓] {ticker} → Zone: {zone} (Price: {latest_close:.4f})\") # Progress print\n", + " \n", + " current_zone_results.append({\n", + " \"Ticker\": ticker,\n", + " \"Current Zone\": zone,\n", + " \"Current Price\": latest_close\n", + " })\n", + " except Exception as e:\n", + " print(f\"[❌] Failed for {ticker}: {e}\")\n", + "\n", + " # Display current zones\n", + " with open(ZONE_STATE_FILE, \"w\") as f:\n", + " json.dump(last_known_zone, f)\n", + " df_current_zones = pd.DataFrame(current_zone_results)\n", + " df_current_zones = df_current_zones.sort_values(by=\"Current Zone\")\n", + " print(\"[✅] Current Zone Snapshot:\\n\")\n", + " print(df_current_zones.to_string(index=False))\n", + " \n", + " # Safe export — ensure folder exists\n", + " #import os\n", + " os.makedirs(\"reports\", exist_ok=True)\n", + "\n", + " outpath = \"reports/current_zone_snapshot.xlsx\"\n", + " df_current_zones.to_excel(outpath, index=False)\n", + " print(f\"[💾] Exported current zone snapshot to: {outpath}\")\n", + " \n", + " return df_current_zones\n", + "\n", + "def export_current_zone_heatmap(df_current_zones, output_path=\"reports/current_zone_snapshot_heatmap.xlsx\"):\n", + " if df_current_zones.empty:\n", + " print(\"[âš ī¸] No current zones to export.\")\n", + " return\n", + "\n", + " print(\"[🎨] Exporting color heatmap version...\")\n", + " with pd.ExcelWriter(output_path, engine=\"xlsxwriter\") as writer:\n", + " df_current_zones.to_excel(writer, sheet_name=\"Current Zones\", index=False)\n", + "\n", + " workbook = writer.book\n", + " worksheet = writer.sheets[\"Current Zones\"]\n", + "\n", + " # Define format rules\n", + " format_premium = workbook.add_format({\"bg_color\": \"#FFD700\", \"bold\": True}) # Gold\n", + " format_fair = workbook.add_format({\"bg_color\": \"#90EE90\"}) # LightGreen\n", + " format_budget = workbook.add_format({\"bg_color\": \"#ADD8E6\"}) # LightBlue\n", + " format_discount = workbook.add_format({\"bg_color\": \"#FF9999\"}) # LightRed\n", + "\n", + " # Apply conditional formats to Current Zone column\n", + " zone_col = df_current_zones.columns.get_loc(\"Current Zone\")\n", + " zone_range = f\"${chr(65 + zone_col)}2:${chr(65 + zone_col)}{len(df_current_zones)+1}\"\n", + "\n", + " worksheet.conditional_format(zone_range, {\"type\": \"text\", \"criteria\": \"containing\", \"value\": \"Premium\", \"format\": format_premium})\n", + " worksheet.conditional_format(zone_range, {\"type\": \"text\", \"criteria\": \"containing\", \"value\": \"Fair\", \"format\": format_fair})\n", + " worksheet.conditional_format(zone_range, {\"type\": \"text\", \"criteria\": \"containing\", \"value\": \"Budget\", \"format\": format_budget})\n", + " worksheet.conditional_format(zone_range, {\"type\": \"text\", \"criteria\": \"containing\", \"value\": \"Discount\", \"format\": format_discount})\n", + "\n", + " print(f\"[💾] Exported color heatmap to: {output_path}\")\n", + "\n", + "#generate_current_zone_snapshot()\n", + "\n", + "if __name__ == \"__main__\":\n", + " #generate_alert_report()\n", + " #generate_current_zone_snapshot()\n", + " df_current_zones = generate_current_zone_snapshot()\n", + " export_current_zone_heatmap(df_current_zones)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/archive/legacy_notebooks/Zone_transition_X.ipynb b/archive/legacy_notebooks/Zone_transition_X.ipynb new file mode 100644 index 0000000..4d2716e --- /dev/null +++ b/archive/legacy_notebooks/Zone_transition_X.ipynb @@ -0,0 +1,370 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[→] Checking USDCAD=X...\n", + "YF.download() has changed argument auto_adjust default to True\n", + "[→] Checking USDGBP=X...\n", + "[→] Checking USDNOK=X...\n", + "[→] Checking USDPLN=X...\n", + "[→] Checking USDAUD=X...\n", + "[→] Checking USDSGD=X...\n", + "[→] Checking USDJPY=X...\n", + "[→] Checking USDZAR=X...\n", + "[→] Checking USDBRL=X...\n", + "[→] Checking EURUSD=X...\n", + "[→] Checking EURGBP=X...\n", + "[→] Checking EURCHF=X...\n", + "[→] Checking EURPLN=X...\n", + "[→] Checking EURCZK=X...\n", + "[→] Checking EURNZD=X...\n", + "[→] Checking EURSEK=X...\n", + "[→] Checking EURZAR=X...\n", + "[→] Checking EURSGD=X...\n", + "[→] Checking GBPNOK=X...\n", + "[→] Checking GBPJPY=X...\n", + "[→] Checking GBPAUD=X...\n", + "[→] Checking GBPCAD=X...\n", + "[→] Checking SEKNOK=X...\n", + "[❌] Failed for SEKNOK=X: can only concatenate str (not \"float\") to str\n", + "[→] Checking SEKJPY=X...\n", + "[→] Checking CHFNOK=X...\n", + "[→] Checking CADNOK=X...\n", + "[→] Checking AUDNZD=X...\n", + "[→] Checking AUDJPY=X...\n", + "[→] Checking AUDSEK=X...\n", + "[→] Checking AUDCAD=X...\n", + "[→] Checking NZDSGD=X...\n", + "[→] Checking NZDCHF=X...\n", + "[→] Checking NZDNOK=X...\n", + "[→] Checking SGDJPY=X...\n", + "[→] Checking SGDHKD=X...\n", + "[→] Checking EURCAD=X...\n", + "[→] Checking USDCHF=X...\n", + "[→] Checking GBPCHF=X...\n", + "[→] Checking EURNOK=X...\n", + "[✅] Summary of Key Level Touches:\n", + "\n", + " Ticker Level Name Level Touches (24h) Most Recent Touch (SAST)\n", + "AUDSEK=X Red Lower 6.2500 3 2025-07-03 08:00\n", + "EURCZK=X Yellow Upper 24.6650 6 2025-07-03 15:00\n", + "EURGBP=X Yellow Upper 0.8616 29 2025-07-04 21:00\n", + "EURNZD=X Purple upper 1.9415 24 2025-07-04 15:00\n", + "EURPLN=X Red Lower 4.2475 8 2025-07-04 14:00\n", + "EURZAR=X Red Upper 20.6880 7 2025-07-04 12:00\n", + "NZDNOK=X Yellow Lower 6.1300 1 2025-07-03 01:00\n", + "SEKJPY=X Purple upper 15.1650 1 2025-07-03 16:00\n", + "USDBRL=X Green 5.4065 10 2025-07-04 18:00\n", + "USDNOK=X Green 10.0550 17 2025-07-04 16:00\n", + "USDZAR=X Yellow Upper 17.6300 5 2025-07-04 18:00\n", + "[→] Preparing current prices and zones for hits...\n", + "[💾] Key Level Hit Log saved to: reports/key_level_hit_log.csv\n", + " Timestamp Ticker Level Name Level Touch Time (UTC) From Zone Current Zone Current Price\n", + "2025-07-04 19:06:04 AUDSEK=X Red Lower 6.2500 2025-07-03 06:00:00 Clearance Discount 6.25740\n", + "2025-07-04 19:06:06 EURCZK=X Yellow Upper 24.6650 2025-07-03 13:00:00 Fair Fair 24.63460\n", + "2025-07-04 19:06:07 EURGBP=X Yellow Upper 0.8616 2025-07-04 19:00:00 Fair Plus+ 0.86264\n", + "2025-07-04 19:06:08 EURNZD=X Purple upper 1.9415 2025-07-04 13:00:00 Premium Premium+ 1.94463\n", + "2025-07-04 19:06:09 EURPLN=X Red Lower 4.2475 2025-07-04 12:00:00 Clearance Clearance 4.24176\n", + "2025-07-04 19:06:10 EURZAR=X Red Upper 20.6880 2025-07-04 10:00:00 Plus+ Premium 20.74473\n", + "2025-07-04 19:06:11 NZDNOK=X Yellow Lower 6.1300 2025-07-02 23:00:00 Discount Discount 6.09720\n", + "2025-07-04 19:06:12 SEKJPY=X Purple upper 15.1650 2025-07-03 14:00:00 Premium Premium 15.11100\n", + "2025-07-04 19:06:13 USDBRL=X Green 5.4065 2025-07-04 16:00:00 Budget Fair 5.42300\n", + "2025-07-04 19:06:15 USDNOK=X Green 10.0550 2025-07-04 14:00:00 Budget Fair 10.06871\n", + "2025-07-04 19:06:16 USDZAR=X Yellow Upper 17.6300 2025-07-04 16:00:00 Fair Fair 17.60540\n" + ] + } + ], + "source": [ + "import yfinance as yf\n", + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "from pathlib import Path\n", + "from collections import defaultdict\n", + "import pytz\n", + "import smtplib\n", + "import os\n", + "\n", + "ZONE_DEFINITIONS = [\n", + " (\"Premium+\", float(\"inf\"), \"Purple upper\"),\n", + " (\"Premium\", \"Purple upper\", \"Red Upper\"),\n", + " (\"Plus+\", \"Red Upper\", \"Yellow Upper\"),\n", + " (\"Fair\", \"Yellow Upper\", \"Green\"),\n", + " (\"Budget\", \"Green\", \"Yellow Lower\"),\n", + " (\"Discount\", \"Yellow Lower\", \"Red Lower\"),\n", + " (\"Clearance\", \"Red Lower\", \"Purple lower\"),\n", + " (\"Reset\", \"Purple lower\", float(\"-inf\")),\n", + "]\n", + "\n", + "# === Current Zone Computation ===\n", + "def compute_current_zone(price, zone_definitions, level_dict):\n", + " # Prepare level name → value lookup\n", + " levels = {}\n", + " for zone_name, a, b in zone_definitions:\n", + " if isinstance(a, str):\n", + " levels[a] = level_dict.get(a, None)\n", + " if isinstance(b, str):\n", + " levels[b] = level_dict.get(b, None)\n", + " levels[\"Purple upper\"] = level_dict.get(\"Purple upper\", float(\"inf\"))\n", + " levels[\"Purple lower\"] = level_dict.get(\"Purple lower\", float(\"-inf\"))\n", + " \n", + " for zone_name, upper_bound, lower_bound in zone_definitions:\n", + " # Resolve upper/lower boundaries\n", + " if isinstance(upper_bound, str):\n", + " upper_value = levels.get(upper_bound, float(\"inf\"))\n", + " else:\n", + " upper_value = upper_bound\n", + " if isinstance(lower_bound, str):\n", + " lower_value = levels.get(lower_bound, float(\"-inf\"))\n", + " else:\n", + " lower_value = lower_bound\n", + " \n", + " # Is price in this zone?\n", + " if lower_value < price <= upper_value:\n", + " return zone_name\n", + " \n", + " return \"Unknown\"\n", + "\n", + "\n", + "# === CONFIG ===\n", + "TICKER_LIST = [\n", + " 'USDCAD=X', 'USDGBP=X', 'USDNOK=X', 'USDPLN=X', 'USDAUD=X', 'USDSGD=X',\n", + " 'USDJPY=X', 'USDZAR=X', 'USDBRL=X', 'EURUSD=X', 'EURGBP=X', 'EURCHF=X',\n", + " 'EURPLN=X', 'EURCZK=X', 'EURNZD=X', 'EURSEK=X', 'EURZAR=X', 'EURSGD=X',\n", + " 'GBPNOK=X', 'GBPJPY=X', 'GBPAUD=X', 'GBPCAD=X', 'SEKNOK=X', 'SEKJPY=X',\n", + " 'CHFNOK=X', 'CADNOK=X', 'AUDNZD=X', 'AUDJPY=X', 'AUDSEK=X', 'AUDCAD=X',\n", + " 'NZDSGD=X', 'NZDCHF=X', 'NZDNOK=X', 'SGDJPY=X', 'SGDHKD=X', 'EURCAD=X',\n", + " 'USDCHF=X', 'GBPCHF=X', 'EURNOK=X'\n", + "]\n", + "\n", + "\n", + "KEY_LEVELS_FILE = Path(r\"C:\\Users\\T460\\Documents\\Quant_trading_research\\Data Packs & Scripts\\Dev_scripts\\FX_1D\\FX_1D_KEY.xlsx\")\n", + "PIP_RANGE = 0.001\n", + "LOOKBACK_HOURS = 24\n", + "since = datetime.utcnow() - timedelta(hours=LOOKBACK_HOURS)\n", + "# === Load Key Levels ===\n", + "def load_key_levels(filepath):\n", + " df = pd.read_excel(filepath)\n", + " df.set_index(\"Ticker\", inplace=True)\n", + " return df\n", + "\n", + "# === Check if price touched a key level (with Level Name) ===\n", + "def check_proximity(level_dict, high, low):\n", + " matches = []\n", + " for level_name, level_value in level_dict.items():\n", + " if pd.isna(level_value):\n", + " continue\n", + " if (low <= level_value + PIP_RANGE) and (high >= level_value - PIP_RANGE):\n", + " matches.append({\n", + " \"Level\": round(level_value, 5),\n", + " \"Level Name\": level_name\n", + " })\n", + " return matches\n", + "\n", + "# === Summarize Touch Events ===\n", + "def summarize_touch_events(touches):\n", + " from collections import defaultdict\n", + " stats_dict = defaultdict(lambda: {\"count\": 0, \"last_touch\": None})\n", + "\n", + " for touch in touches:\n", + " key = (touch[\"Ticker\"], touch[\"Level\"], touch[\"Level Name\"])\n", + " stats_dict[key][\"count\"] += 1\n", + " stats_dict[key][\"last_touch\"] = touch[\"Time\"]\n", + "\n", + " rows = []\n", + " for (ticker, level, level_name), stats in stats_dict.items():\n", + " sast_time = stats[\"last_touch\"] + timedelta(hours=2)\n", + " rows.append({\n", + " \"Ticker\": ticker,\n", + " \"Level Name\": level_name,\n", + " \"Level\": level,\n", + " \"Touches (24h)\": stats[\"count\"],\n", + " \"Most Recent Touch (SAST)\": sast_time.strftime(\"%Y-%m-%d %H:%M\")\n", + " })\n", + "\n", + " return pd.DataFrame(rows)\n", + "\n", + "# === Main Alert Generator ===\n", + "def generate_alert_report():\n", + " global since\n", + " if 'since' not in globals():\n", + " LOOKBACK_HOURS = 24\n", + " since = datetime.utcnow() - timedelta(hours=LOOKBACK_HOURS)\n", + " \n", + " key_levels_df = load_key_levels(KEY_LEVELS_FILE)\n", + " touches = []\n", + "\n", + " for ticker in TICKER_LIST:\n", + " print(f\"[→] Checking {ticker}...\")\n", + "\n", + " try:\n", + " if datetime.utcnow().weekday() >= 5:\n", + " print(f\"[â„šī¸] Skipping {ticker} → Weekend\")\n", + " continue\n", + "\n", + " data = yf.download(ticker, start=since.strftime('%Y-%m-%d'), interval=\"1h\", progress=False)\n", + " if data.empty:\n", + " print(f\"[âš ī¸] No data for {ticker} — likely weekend or market closed.\")\n", + " continue\n", + " data = data.dropna()\n", + "\n", + " short = ticker.split(\"=\")[0] + \"=X\" if \"=X\" in ticker else ticker\n", + " levels_series = key_levels_df.loc[short].dropna()\n", + " level_dict = dict(levels_series)\n", + "\n", + " for ts, row in data.iterrows():\n", + " matches = check_proximity(level_dict, row.High.item(), row.Low.item())\n", + " if matches:\n", + " for match in matches:\n", + " touches.append({\n", + " \"Ticker\": ticker,\n", + " \"Level\": match[\"Level\"],\n", + " \"Level Name\": match[\"Level Name\"],\n", + " \"Time\": ts\n", + " })\n", + "\n", + " except Exception as e:\n", + " print(f\"[❌] Failed for {ticker}: {e}\")\n", + "\n", + " if not touches:\n", + " print(\"[✅] No key levels touched in past 24 hours.\")\n", + " alert_df = pd.DataFrame() # empty df if no touches\n", + " else:\n", + " alert_df = summarize_touch_events(touches)\n", + " alert_df = alert_df.sort_values(by=[\"Ticker\", \"Level Name\"])\n", + " print(\"[✅] Summary of Key Level Touches:\\n\")\n", + " print(alert_df.to_string(index=False))\n", + "\n", + " # Return BOTH alert_df and key_levels_df\n", + " return alert_df, key_levels_df\n", + "\n", + "# === PHASE 1B → Key Level Hit Log with From Zone ===\n", + "\n", + "# === FINAL V2 SAFE PATCH → log_key_level_hits() ===\n", + "def log_key_level_hits(alert_df, key_levels_df):\n", + " if alert_df.empty:\n", + " print(\"[âš ī¸] No key level hits to log.\")\n", + " return\n", + " \n", + " # Load current prices → to compute current zone\n", + " print(\"[→] Preparing current prices and zones for hits...\")\n", + " \n", + " os.makedirs(\"reports\", exist_ok=True)\n", + " \n", + " log_records = []\n", + " for _, row in alert_df.iterrows():\n", + " ticker = row[\"Ticker\"]\n", + " level_name = row[\"Level Name\"]\n", + " level_value = row[\"Level\"]\n", + " touch_time = row[\"Most Recent Touch (SAST)\"]\n", + "\n", + " try:\n", + " # Try 1h first → fallback to daily\n", + " data = yf.download(ticker, period=\"1d\", interval=\"1h\", progress=False)\n", + " if data.empty:\n", + " print(f\"[âš ī¸] No 1H data for {ticker} → trying Daily...\")\n", + " data = yf.download(ticker, period=\"5d\", interval=\"1d\", progress=False)\n", + "\n", + " if data.empty:\n", + " print(f\"[â„šī¸] Skipping {ticker} → Market likely closed (no data)\")\n", + " continue\n", + " \n", + " data = data.dropna()\n", + "\n", + " current_price = data[\"Close\"].iloc[-1].item()\n", + "\n", + " # Prepare level dict\n", + " short = ticker.split(\"=\")[0] + \"=X\" if \"=X\" in ticker else ticker\n", + " levels_series = key_levels_df.loc[short].dropna()\n", + " level_dict = dict(levels_series)\n", + "\n", + " # Current Zone\n", + " current_zone = compute_current_zone(current_price, ZONE_DEFINITIONS, level_dict)\n", + "\n", + " # From Zone → assume level_value is approximate price at touch\n", + " from_zone = compute_current_zone(level_value, ZONE_DEFINITIONS, level_dict)\n", + "\n", + " log_records.append({\n", + " \"Timestamp\": pd.Timestamp.utcnow().strftime(\"%Y-%m-%d %H:%M:%S\"),\n", + " \"Ticker\": ticker,\n", + " \"Level Name\": level_name,\n", + " \"Level\": level_value,\n", + " \"Touch Time (UTC)\": pd.to_datetime(touch_time).tz_localize('Africa/Johannesburg').tz_convert('UTC').strftime(\"%Y-%m-%d %H:%M:%S\"),\n", + " \"From Zone\": from_zone,\n", + " \"Current Zone\": current_zone,\n", + " \"Current Price\": current_price\n", + " })\n", + " \n", + " except Exception as e:\n", + " print(f\"[❌] Failed for {ticker}: {e}\")\n", + "\n", + " df_log = pd.DataFrame(log_records)\n", + " \n", + " # Append to CSV log\n", + " log_file = \"reports/key_level_hit_log.csv\"\n", + " df_log.to_csv(log_file, mode=\"a\", index=False, header=not os.path.exists(log_file))\n", + " \n", + " print(f\"[💾] Key Level Hit Log saved to: {log_file}\")\n", + " print(df_log.to_string(index=False))\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " alert_df, key_levels_df = generate_alert_report()\n", + " log_key_level_hits(alert_df, key_levels_df)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(ZONE_DEFINITIONS)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/archive/old_pages/FX_Heatmap.py b/archive/old_pages/FX_Heatmap.py new file mode 100644 index 0000000..630e937 --- /dev/null +++ b/archive/old_pages/FX_Heatmap.py @@ -0,0 +1,58 @@ +# Quant_framework/app/FX_Heatmap.py +import streamlit as st +import pandas as pd +import numpy as np +import yfinance as yf +from datetime import datetime +import os +from viz.fx_heatmap import fx_heatmap +fx_heatmap() + + + +st.title("📊 FX Heatmap Test Page") +st.write("If you see this, multipage tabs are working.") + +CURRENCY_LIST = ['USD','CAD','EUR','GBP','JPY','AUD','NZD','CHF','SGD','NOK'] +EXPORT_TO_EXCEL = True +OUTPUT_FILE = "reports/fx_major_heatmap.xlsx" +TODAY_DATE = datetime.utcnow().strftime('%Y-%m-%d') + +matrix = pd.DataFrame(index=CURRENCY_LIST, columns=CURRENCY_LIST, dtype=float) + +@st.cache_data(show_spinner=False) +def get_daily_pct_change(ticker): + try: + data = yf.download(ticker, period="2d", interval="1d", progress=False) + if len(data) < 2: + return None + open_val = data['Open'].iloc[-1].item() + close_val = data['Close'].iloc[-1].item() + return (close_val - open_val) / open_val * 100 + except: + return None + +with st.spinner("Fetching data from Yahoo Finance..."): + for base in CURRENCY_LIST: + for quote in CURRENCY_LIST: + if base == quote: + matrix.at[base, quote] = np.nan + continue + pair = f"{base}{quote}=X" + pct = get_daily_pct_change(pair) + if pct is not None: + matrix.at[base, quote] = round(pct, 2) + +st.subheader(f"% Change Matrix for {TODAY_DATE}") +st.dataframe(matrix.style.background_gradient(cmap="RdYlGn", axis=None).format("{:.2f}"), use_container_width=True) + +# === Compute strength === +strength_scores = pd.Series(dtype=float) +for ccy in CURRENCY_LIST: + row_mean = matrix.loc[ccy].mean(skipna=True) + col_mean = matrix[ccy].mean(skipna=True) + strength = row_mean - col_mean + strength_scores[ccy] = round(strength, 2) + +st.subheader("âš–ī¸ Currency Strength Meter") +st.bar_chart(strength_scores.sort_values(ascending=True)) diff --git a/archive/old_pages/Home.py b/archive/old_pages/Home.py new file mode 100644 index 0000000..ee8f456 --- /dev/null +++ b/archive/old_pages/Home.py @@ -0,0 +1,5 @@ +# Quant_framework/app/Home.py +import streamlit as st +st.set_page_config(page_title="Quant Dashboard", layout="wide") +st.title("Quant Framework") +st.markdown("Welcome! Select a tab on the left to begin.") diff --git a/archive/old_pages/Strategy_Stats.py b/archive/old_pages/Strategy_Stats.py new file mode 100644 index 0000000..e69de29 diff --git a/archive/old_pages/Strength_Meter.py b/archive/old_pages/Strength_Meter.py new file mode 100644 index 0000000..e69de29 diff --git a/archive/old_pages/Top_Movers.py b/archive/old_pages/Top_Movers.py new file mode 100644 index 0000000..e69de29 diff --git a/archive/old_pages/Zone_Locator.py b/archive/old_pages/Zone_Locator.py new file mode 100644 index 0000000..5cb4908 --- /dev/null +++ b/archive/old_pages/Zone_Locator.py @@ -0,0 +1,22 @@ +# File: pages/Zone_Locator.py +import streamlit as st +import pandas as pd +from core.zone_locator import generate_current_zone_snapshot + +st.set_page_config(page_title="Zone Locator", layout="wide") +st.title("📍 Zone Locator") + +with st.spinner("Computing current zones..."): + df_zones = generate_current_zone_snapshot() + +if df_zones.empty: + st.warning("No zone data available. Check data sources.") +else: + st.success("Zone snapshot generated!") + st.dataframe(df_zones, use_container_width=True) + + zone_counts = df_zones['Current Zone'].value_counts().reset_index() + zone_counts.columns = ['Zone', 'Tickers in Zone'] + + st.subheader("📊 Zone Distribution") + st.bar_chart(zone_counts.set_index('Zone')) diff --git a/archive/old_pages/Zone_Transition.py b/archive/old_pages/Zone_Transition.py new file mode 100644 index 0000000..287f9fa --- /dev/null +++ b/archive/old_pages/Zone_Transition.py @@ -0,0 +1,92 @@ +import streamlit as st +import pandas as pd +from datetime import datetime +from core.zone_transition import get_zone_transitions_today + +TAB_NAME = "📈 Zone Transitions" + +def render(): + st.header("📈 Zone Transitions (Past 24h)") + + # Add a refresh button + col1, col2 = st.columns([1, 4]) + with col1: + refresh_data = st.button("🔄 Refresh Data", help="Scan for new zone transitions") + + # Check if it's weekend + if datetime.now().weekday() >= 5: + st.warning("â¸ī¸ Markets are closed on weekends. Showing cached data if available.") + + # Get live data or use cached data + with st.spinner("Scanning tickers for zone transitions..."): + if refresh_data or 'zone_transitions_cache' not in st.session_state: + df_transitions = get_zone_transitions_today() + st.session_state.zone_transitions_cache = df_transitions + else: + df_transitions = st.session_state.zone_transitions_cache + + if df_transitions.empty: + st.info("No zone transitions detected in the last 24 hours.") + + # Optionally show historical data from CSV + st.subheader("📋 Historical Data") + show_historical = st.checkbox("Show historical transitions from log file") + + if show_historical: + try: + import os + log_file = "reports/zone_transition_log.csv" + if os.path.exists(log_file): + df_historical = pd.read_csv(log_file) + + # Standardize column names + if 'Date' in df_historical.columns and 'Timestamp' not in df_historical.columns: + df_historical = df_historical.rename(columns={'Date': 'Timestamp'}) + + if 'Timestamp' in df_historical.columns: + df_historical['Timestamp'] = pd.to_datetime(df_historical['Timestamp']) + df_historical = df_historical.sort_values(by='Timestamp', ascending=False) + + # Show last 50 transitions + st.dataframe(df_historical.head(50), use_container_width=True) + st.caption(f"Showing last 50 of {len(df_historical)} total historical transitions") + else: + st.info("No historical data file found.") + except Exception as e: + st.error(f"Could not load historical data: {e}") + else: + st.success(f"✅ {len(df_transitions)} transitions found in the last 24 hours!") + + # Display the fresh data + st.dataframe(df_transitions, use_container_width=True) + + # Add some analytics + if len(df_transitions) > 0: + col1, col2, col3 = st.columns(3) + + with col1: + unique_tickers = df_transitions['Ticker'].nunique() + st.metric("đŸˇī¸ Active Tickers", unique_tickers) + + with col2: + most_active = df_transitions['Ticker'].value_counts().iloc[0] if len(df_transitions) > 0 else 0 + st.metric("đŸ”Ĩ Max Transitions", most_active) + + with col3: + latest_time = df_transitions['Timestamp'].max() + hours_ago = (datetime.now() - latest_time.replace(tzinfo=None)).total_seconds() / 3600 + st.metric("⏰ Latest Transition", f"{hours_ago:.1f}h ago") + + # Ticker breakdown + st.subheader("📊 Transitions by Ticker") + ticker_counts = df_transitions['Ticker'].value_counts() + st.bar_chart(ticker_counts) + + # Download button + csv = df_transitions.to_csv(index=False).encode("utf-8") + st.download_button( + "đŸ“Ĩ Download Current Transitions", + csv, + file_name=f"zone_transitions_{datetime.now().strftime('%Y%m%d_%H%M')}.csv", + mime="text/csv" + ) diff --git a/archive/old_pages/config.py b/archive/old_pages/config.py new file mode 100644 index 0000000..e69de29 diff --git a/archive/test_files/main.py b/archive/test_files/main.py new file mode 100644 index 0000000..37fa0ac --- /dev/null +++ b/archive/test_files/main.py @@ -0,0 +1,41 @@ +# main.py + +# main.py + +from core.runner import run_all_strategies +from core.strategy_registry import STRATEGY_REGISTRY + +if __name__ == "__main__": + # Tickers to test + tickers = [ + "CADCHF=X", + "GBPNOK=X", + "NZDUSD=X" + ] + + # Strategies to run + STRATEGIES_TO_RUN = ["macd_crossover"] # Add more names as needed + + # Filter only selected strategies + filtered_registry = { + name: func for name, func in STRATEGY_REGISTRY.items() + if name in STRATEGIES_TO_RUN + } + + # Date range + start_date = "2025-05-07" + end_date = "2025-06-18" + + # Run all + summary_df = run_all_strategies( + tickers=tickers, + strategy_registry=filtered_registry, + start_date=start_date, + end_date=end_date, + atr_mult=1.5, + max_bars=20, + export=True + ) + + print("\n=== FINAL SUMMARY ===") + print(summary_df) diff --git a/archive/test_files/test_csv_loader.py b/archive/test_files/test_csv_loader.py new file mode 100644 index 0000000..b7938d6 --- /dev/null +++ b/archive/test_files/test_csv_loader.py @@ -0,0 +1,40 @@ +import pandas as pd +import os + +# Path to your CSVs +CSV_FOLDER = r"C:\Users\T460\Documents\Quant_trading_research\Quant_framework\data\csv_data" +TICKER = "CADCHF=X" +START_DATE = "2025-06-01" +END_DATE = "2025-06-15" + +filepath = os.path.join(CSV_FOLDER, f"{TICKER}.csv") +print(f"[📄] Loading file: {filepath}") + +try: + df = pd.read_csv(filepath) + print("[🔍] Raw columns:", df.columns.tolist()) + print("[đŸ§Ē] First raw Date values:", df['Date'].head(5).tolist()) + + + # Clean column names + df.columns = df.columns.str.strip().str.replace('\ufeff', '') + + # Parse datetime from MM/DD/YYYY HH:MM format + df['Date'] = pd.to_datetime(df['Date'], format="%m/%d/%Y %H:%M", errors='raise') + df.dropna(subset=['Date'], inplace=True) + df.set_index('Date', inplace=True) + + print("[📅] Index preview:", df.index.min(), "→", df.index.max()) + + # Fix comma decimal and cast price columns + for col in ['Open', 'High', 'Low', 'Close']: + df[col] = df[col].astype(str).str.replace(',', '.').astype(float) + + # Apply date filter + df_filtered = df.loc[START_DATE:END_DATE] + print("[✅] Filtered rows:", df_filtered.shape[0]) + print(df_filtered.head()) + +except Exception as e: + print("[âš ī¸] Strict format failed, falling back to auto detection.") + df['Date'] = pd.to_datetime(df['Date'], errors='coerce') diff --git a/archive/unused_core/__init__.py b/archive/unused_core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archive/unused_core/clean_zone_log.py b/archive/unused_core/clean_zone_log.py new file mode 100644 index 0000000..98ed9cb --- /dev/null +++ b/archive/unused_core/clean_zone_log.py @@ -0,0 +1,35 @@ +import os + +INPUT_FILE = "reports/zone_transition_log.csv" +OUTPUT_FILE = "reports/zone_transition_log_cleaned.csv" +EXPECTED_FIELDS = 4 + +def clean_zone_transition_log(input_path, output_path, expected_fields=4): + if not os.path.exists(input_path): + print(f"[❌] File not found: {input_path}") + return + + with open(input_path, "r") as infile: + lines = infile.readlines() + + good_lines = [] + bad_lines = [] + + for i, line in enumerate(lines, start=1): + if line.count(",") == expected_fields - 1: + good_lines.append(line) + else: + bad_lines.append((i, line.strip())) + + with open(output_path, "w") as outfile: + outfile.writelines(good_lines) + + print(f"[✅] Cleaned log saved to: {output_path}") + print(f"[📄] Valid rows kept: {len(good_lines)}") + if bad_lines: + print(f"[âš ī¸] Skipped {len(bad_lines)} malformed rows:") + for idx, bad in bad_lines: + print(f" Line {idx}: {bad}") + +if __name__ == "__main__": + clean_zone_transition_log(INPUT_FILE, OUTPUT_FILE) diff --git a/archive/unused_core/metrics.py b/archive/unused_core/metrics.py new file mode 100644 index 0000000..41b1cfe --- /dev/null +++ b/archive/unused_core/metrics.py @@ -0,0 +1,38 @@ +# core/metrics.py + +import pandas as pd +import numpy as np + +def compute_fx_metrics(results_df: pd.DataFrame) -> dict: + if results_df.empty: + return {} + + results_df['Return_%'] = (results_df['Exit_Price'] - results_df['Entry_Price']) / results_df['Entry_Price'] * 100 + results_df['Pips'] = (results_df['Exit_Price'] - results_df['Entry_Price']) * 10000 + + equity = results_df['Return_%'].cumsum() + peak = equity.cummax() + drawdown = peak - equity + max_dd = drawdown.max() + + win_trades = results_df[results_df['Result'] == 'Win'] + loss_trades = results_df[results_df['Result'] == 'Loss'] + + total_return = results_df['Return_%'].sum() + avg_return = results_df['Return_%'].mean() + std_return = results_df['Return_%'].std() + sharpe = (avg_return / std_return) * np.sqrt(252) if std_return else 0 + win_rate = len(win_trades) / len(results_df) if len(results_df) else 0 + expectancy = (win_rate * win_trades['Return_%'].mean()) + ((1 - win_rate) * loss_trades['Return_%'].mean()) if not win_trades.empty and not loss_trades.empty else 0 + profit_factor = win_trades['Return_%'].sum() / abs(loss_trades['Return_%'].sum()) if not loss_trades.empty else np.inf + + return { + "Total Trades": len(results_df), + "Total Return %": round(total_return, 2), + "Avg Return %": round(avg_return, 2), + "Sharpe Ratio": round(sharpe, 2), + "Max Drawdown %": round(max_dd, 2), + "Profit Factor": round(profit_factor, 2), + "Expectancy": round(expectancy, 2), + "Win Rate": f"{win_rate:.2%}" + } diff --git a/archive/unused_core/pct_change_heatmap.py b/archive/unused_core/pct_change_heatmap.py new file mode 100644 index 0000000..c028b06 --- /dev/null +++ b/archive/unused_core/pct_change_heatmap.py @@ -0,0 +1,67 @@ +import yfinance as yf +import pandas as pd +import numpy as np +import os +from datetime import datetime, timezone + +# === CONFIG === +EXPORT_TO_EXCEL = True +OUTPUT_FILE = "reports/fx_major_heatmap.xlsx" +CURRENCY_LIST = ['USD','CAD', 'EUR', 'GBP', 'CHF', 'NOK', 'SGD','JPY', 'AUD', 'NZD'] +TODAY_DATE = datetime.now(timezone.utc).strftime('%Y-%m-%d') + +# === Initialize matrix === +def get_daily_pct_change(ticker): + try: + data = yf.download(ticker, period="2d", interval="1d", progress=False, auto_adjust=False) + if len(data) < 2: + return None + open_val = data['Open'].iloc[-1].item() + close_val = data['Close'].iloc[-1].item() + return (close_val - open_val) / open_val * 100 + except Exception as e: + print(f"[âš ī¸] Error fetching {ticker}: {e}") + return None + +def generate_fx_change_heatmap(): + matrix = pd.DataFrame(index=CURRENCY_LIST, columns=CURRENCY_LIST, dtype=float) + for base in CURRENCY_LIST: + for quote in CURRENCY_LIST: + if base == quote: + matrix.at[base, quote] = np.nan + continue + pair = f"{base}{quote}=X" + pct_change = get_daily_pct_change(pair) + if pct_change is not None: + matrix.at[base, quote] = round(pct_change, 2) + + print(f"[📊] FX % Change Heatmap for {TODAY_DATE}") + print(matrix) + + if EXPORT_TO_EXCEL: + os.makedirs("reports", exist_ok=True) + with pd.ExcelWriter(OUTPUT_FILE, engine='xlsxwriter') as writer: + matrix.to_excel(writer, sheet_name='Heatmap') + workbook = writer.book + worksheet = writer.sheets['Heatmap'] + fmt = workbook.add_format({'num_format': '0.00', 'align': 'center'}) + + last_row = len(matrix) + 1 + last_col = len(matrix.columns) + col_letter_start = chr(ord('A') + 1) + col_letter_end = chr(ord('A') + last_col) + zone_range = f"{col_letter_start}2:{col_letter_end}{last_row}" + + worksheet.conditional_format(zone_range, { + 'type': '3_color_scale', + 'min_color': "#63BE7B", + 'mid_color': "#FFEB84", + 'max_color': "#F8696B", + }) + print(f"[💾] Exported FX heatmap to: {OUTPUT_FILE}") + return matrix + +if __name__ == "__main__": + generate_fx_change_heatmap() + print("✅ FX Change Heatmap script loaded with no syntax errors.") + \ No newline at end of file diff --git a/archive/unused_core/runner.py b/archive/unused_core/runner.py new file mode 100644 index 0000000..f9b9acc --- /dev/null +++ b/archive/unused_core/runner.py @@ -0,0 +1,60 @@ +# core/runner.py + +import os +import pandas as pd +from core.strategy_engine import run_strategy_on_ticker, plot_equity_curve +from core.metrics import compute_fx_metrics +from data.local_loader import load_local_csv + + +def run_all_strategies(tickers: list[str], strategy_registry: dict, + start_date: str, end_date: str, + atr_mult: float = 1.5, max_bars: int = 20, + export: bool = True) -> pd.DataFrame: + + all_results = [] + os.makedirs("reports", exist_ok=True) + + for strategy_name, strategy_func in strategy_registry.items(): + print(f"\n[🚀] Running strategy: {strategy_name}") + + for ticker in tickers: + print(f" → Ticker: {ticker}") + + df = load_local_csv(ticker, start_date=start_date, end_date=end_date) + if df.empty: + print(f"[âš ī¸] No data for {ticker}, skipping.") + continue + + trade_log, metrics = run_strategy_on_ticker( + df, strategy_func, strategy_name + f"_{ticker}", + atr_mult=atr_mult, max_bars=max_bars + ) + + if trade_log.empty or not metrics: + continue + + metrics['Strategy_Ticker'] = f"{strategy_name}_{ticker}" + all_results.append(metrics) + + if export: + outpath = f"reports/strategy_report_{strategy_name.lower()}_{ticker.lower()}.xlsx" + chart_path = f"reports/strategy_report_{strategy_name.lower()}_{ticker.lower()}_equity_curve.png" + plot_equity_curve(trade_log, strategy_name, chart_path) + + with pd.ExcelWriter(outpath, engine='xlsxwriter') as writer: + trade_log.to_excel(writer, sheet_name='Trades', index=False) + pd.DataFrame([metrics]).to_excel(writer, sheet_name='Metrics', index=False) + worksheet = writer.book.add_worksheet('EquityCurve') + writer.sheets['EquityCurve'] = worksheet + worksheet.insert_image('B2', chart_path) + + print(f"[💾] Exported report: {outpath}") + + summary_df = pd.DataFrame(all_results) + if export and not summary_df.empty: + summary_path = "reports/strategy_summary.xlsx" + summary_df.to_excel(summary_path, index=False) + print(f"[📊] Summary exported to {summary_path}") + + return summary_df diff --git a/archive/unused_core/strategies/macd_strategy.py b/archive/unused_core/strategies/macd_strategy.py new file mode 100644 index 0000000..33d336b --- /dev/null +++ b/archive/unused_core/strategies/macd_strategy.py @@ -0,0 +1,81 @@ +# core/strategies/macd_strategy.py + +# core/strategies/macd_crossover.py + +import pandas as pd +from core.strategy_registry import register_strategy + +@register_strategy +def macd_crossover(df: pd.DataFrame, atr_mult: float = 1.5, max_bars: int = 20): + df = df.copy() + df = df.sort_index() + + df['EMA12'] = df['Close'].ewm(span=12, adjust=False).mean() + df['EMA26'] = df['Close'].ewm(span=26, adjust=False).mean() + df['MACD'] = df['EMA12'] - df['EMA26'] + df['Signal'] = df['MACD'].ewm(span=9, adjust=False).mean() + df['ATR'] = (df['High'] - df['Low']).rolling(window=14).mean() + + trades = [] + in_position = False + bars_in_trade = 0 + direction = None + + for i in range(1, len(df)): + row = df.iloc[i] + prev = df.iloc[i - 1] + + # Long entry condition: MACD crosses above Signal, and MACD < 0 + if not in_position: + if prev['MACD'] < prev['Signal'] and row['MACD'] > row['Signal'] and row['MACD'] < 0: + entry_price = row['Close'] + atr = row['ATR'] + sl = entry_price - atr * atr_mult + tp = entry_price + atr * atr_mult + entry_time = df.index[i] + reason = "MACD Bull Crossover" + direction = 'long' + in_position = True + bars_in_trade = 0 + + # Short entry condition: MACD crosses below Signal, and MACD > 0 + elif prev['MACD'] > prev['Signal'] and row['MACD'] < row['Signal'] and row['MACD'] > 0: + entry_price = row['Close'] + atr = row['ATR'] + sl = entry_price + atr * atr_mult + tp = entry_price - atr * atr_mult + entry_time = df.index[i] + reason = "MACD Bear Crossover" + direction = 'short' + in_position = True + bars_in_trade = 0 + + elif in_position: + bars_in_trade += 1 + + if direction == 'long': + if row['Low'] <= sl: + trades.append({"Entry_Date": entry_time, "Entry_Price": entry_price, "SL": sl, "TP": tp, + "Exit_Date": df.index[i], "Exit_Price": sl, "Result": "Loss", "Reason": reason}) + in_position = False + elif row['High'] >= tp: + trades.append({"Entry_Date": entry_time, "Entry_Price": entry_price, "SL": sl, "TP": tp, + "Exit_Date": df.index[i], "Exit_Price": tp, "Result": "Win", "Reason": reason}) + in_position = False + + elif direction == 'short': + if row['High'] >= sl: + trades.append({"Entry_Date": entry_time, "Entry_Price": entry_price, "SL": sl, "TP": tp, + "Exit_Date": df.index[i], "Exit_Price": sl, "Result": "Loss", "Reason": reason}) + in_position = False + elif row['Low'] <= tp: + trades.append({"Entry_Date": entry_time, "Entry_Price": entry_price, "SL": sl, "TP": tp, + "Exit_Date": df.index[i], "Exit_Price": tp, "Result": "Win", "Reason": reason}) + in_position = False + + if in_position and bars_in_trade >= max_bars: + trades.append({"Entry_Date": entry_time, "Entry_Price": entry_price, "SL": sl, "TP": tp, + "Exit_Date": df.index[i], "Exit_Price": row['Close'], "Result": "Timeout", "Reason": reason}) + in_position = False + + return trades diff --git a/archive/unused_core/strategy_engine.py b/archive/unused_core/strategy_engine.py new file mode 100644 index 0000000..4aa0000 --- /dev/null +++ b/archive/unused_core/strategy_engine.py @@ -0,0 +1,45 @@ +# core/strategy_engine.py + +import pandas as pd +import numpy as np +import os +import matplotlib.pyplot as plt +from core.metrics import compute_fx_metrics + + +def run_strategy_on_ticker(df: pd.DataFrame, strategy_func, strategy_name: str, + atr_mult: float = 1.5, max_bars: int = 20) -> tuple[pd.DataFrame, dict]: + df = df.copy() + df.sort_index(inplace=True) + + trades = strategy_func(df, atr_mult=atr_mult, max_bars=max_bars) + if not trades: + print(f"[âš ī¸] No trades for {strategy_name}") + return pd.DataFrame(), {} + + results_df = pd.DataFrame(trades) + results_df['PnL'] = (results_df['Exit_Price'] - results_df['Entry_Price']) * 10000 # in pips + results_df['Result'] = results_df['PnL'].apply(lambda x: 'Win' if x > 0 else 'Loss' if x < 0 else 'Timeout') + + metrics = compute_fx_metrics(results_df) + + return results_df, metrics + + +def plot_equity_curve(results_df: pd.DataFrame, strategy_name: str, output_path: str = None): + equity = results_df['PnL'].cumsum() + fig, ax = plt.subplots(figsize=(8, 4)) + ax.plot(equity, color='dodgerblue', linewidth=2) + ax.set_title(f'Equity Curve – {strategy_name}') + ax.set_ylabel('Cumulative PnL (Pips)') + ax.set_xlabel('Trade Index') + ax.grid(True) + + if output_path: + os.makedirs(os.path.dirname(output_path), exist_ok=True) + plt.tight_layout() + fig.savefig(output_path) + print(f"[📈] Saved equity curve: {output_path}") + + plt.close(fig) + return fig diff --git a/archive/unused_core/strategy_registry.py b/archive/unused_core/strategy_registry.py new file mode 100644 index 0000000..9bdcf1a --- /dev/null +++ b/archive/unused_core/strategy_registry.py @@ -0,0 +1,12 @@ +# core/strategy_registry.py + +# === Global strategy registry === +STRATEGY_REGISTRY = {} + +def register_strategy(func): + """ + Decorator to register a strategy function with a global strategy registry. + Each strategy must accept a DataFrame and return a list of trade dicts. + """ + STRATEGY_REGISTRY[func.__name__] = func + return func diff --git a/archive/unused_core/top_movers.py b/archive/unused_core/top_movers.py new file mode 100644 index 0000000..0d2b001 --- /dev/null +++ b/archive/unused_core/top_movers.py @@ -0,0 +1,78 @@ +import yfinance as yf +import pandas as pd +import os +from datetime import datetime, timezone + +# === CONFIG === +LOOKBACK_DAYS = 10 +STD_THRESHOLD = 1.5 +EXT_LOG_FILE = "reports/extension_alert_log.csv" +TICKERS = [ + 'USDCAD=X', 'USDGBP=X', 'USDNOK=X', 'USDPLN=X', 'USDAUD=X', 'USDSGD=X', + 'USDJPY=X', 'USDZAR=X', 'USDBRL=X', 'EURUSD=X', 'EURGBP=X', 'EURCHF=X', + 'EURPLN=X', 'EURCZK=X', 'EURNZD=X', 'EURSEK=X', 'EURZAR=X', 'EURSGD=X', + 'GBPNOK=X', 'GBPJPY=X', 'GBPAUD=X', 'GBPCAD=X', 'SEKNOK=X', 'SEKJPY=X', + 'CHFNOK=X', 'CADNOK=X', 'AUDNZD=X', 'AUDJPY=X', 'AUDSEK=X', 'AUDCAD=X', + 'NZDSGD=X', 'NZDCHF=X', 'NZDNOK=X', 'SGDJPY=X', 'SGDHKD=X', 'EURCAD=X', + 'USDCHF=X', 'GBPCHF=X', 'EURNOK=X' +] + + +def get_unusual_movers(tickers, lookback_days, std_threshold): + unusual_movers = [] + for ticker in tickers: + try: + data = yf.download(ticker, period=f"{lookback_days + 2}d", interval='1d', progress=False, auto_adjust=False) + data['Pct Change'] = data['Close'].pct_change() * 100 + + if len(data) < lookback_days + 1 or data['Pct Change'].isna().all(): + print(f"[âš ī¸] Not enough data for {ticker} — skipping.") + continue + + last_date = data.index[-1].date() + if last_date < datetime.now().date(): + print(f"[â„šī¸] {ticker} has no new daily candle today — skipping.") + continue + + recent_changes = data['Pct Change'].iloc[-(lookback_days+1):-1] # Exclude today + today_change = data['Pct Change'].iloc[-1] + + avg = recent_changes.mean() + std = recent_changes.std() + + if abs(today_change) > avg + std_threshold * std: + unusual_movers.append({ + 'Ticker': ticker, + 'Today % Change': round(today_change, 2), + 'Avg % Change': round(avg, 2), + 'Std Dev': round(std, 2), + 'Z-Score': round((today_change - avg)/std, 2) + }) + except Exception as e: + print(f"[âš ī¸] Failed to fetch data for {ticker}: {e}") + + return pd.DataFrame(unusual_movers) + +def run_overextension_scan(): + today = datetime.now().date() + if today.weekday() >= 5: + print("⏸ Market closed (Weekend) — skipping mover scan") + return pd.DataFrame() + + df_extensions = get_unusual_movers(TICKERS, LOOKBACK_DAYS, STD_THRESHOLD) + df_extensions["Timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + + os.makedirs("reports", exist_ok=True) + df_extensions.to_csv(EXT_LOG_FILE, mode="a", index=False, header=not os.path.exists(EXT_LOG_FILE)) + + if df_extensions.empty: + print("[✅] No unusual extension movers found today.") + else: + print("[✅] Unusual Extension Movers:") + print(df_extensions.sort_values(by='Z-Score', ascending=False).to_string(index=False)) + + return df_extensions + +if __name__ == "__main__": + run_overextension_scan() + diff --git a/archive/unused_core/zone_transition.py b/archive/unused_core/zone_transition.py new file mode 100644 index 0000000..0744fe8 --- /dev/null +++ b/archive/unused_core/zone_transition.py @@ -0,0 +1,137 @@ +import yfinance as yf +import pandas as pd +from datetime import datetime, timedelta, timezone +from pathlib import Path +import os + +# === CONFIG === +TICKER_LIST = [ + 'GBPNZD=X', 'EURCHF=X', 'NZDCAD=X', 'USDZAR=X', 'CADCHF=X', + 'GBPJPY=X', 'AUDNZD=X', 'GBPCHF=X', 'USDCAD=X', 'CADJPY=X', + 'AUDJPY=X', 'EURUSD=X', 'EURGBP=X', 'USDNOK=X', 'NOKSEK=X' +] +KEY_LEVELS_FILE = Path("C:/Users/T460/Documents/Quant_trading_research/key_levels_x_3/Key_levels_1D.xlsx") +PIP_RANGE = 0.001 +ZONE_DEFINITIONS = [ + ("Premium+", float("inf"), "Purple upper"), + ("Premium", "Purple upper", "Red Upper"), + ("Plus+", "Red Upper", "Yellow Upper"), + ("Fair", "Yellow Upper", "Green"), + ("Budget", "Green", "Yellow Lower"), + ("Discount", "Yellow Lower", "Red Lower"), + ("Clearance", "Red Lower", "Purple lower"), + ("Reset", "Purple lower", float("-inf")) +] + +# === Utility Functions === +def load_key_levels(filepath): + df = pd.read_excel(filepath) + df.set_index("Ticker", inplace=True) + return df + +def check_proximity(level_dict, high, low): + matches = [] + for level_name, level_value in level_dict.items(): + if pd.isna(level_value): + continue + if (low <= level_value + PIP_RANGE) and (high >= level_value - PIP_RANGE): + matches.append({"Level": round(level_value, 5), "Level Name": level_name}) + return matches + +def compute_current_zone(price, zone_definitions, level_dict): + levels = {} + for _, a, b in zone_definitions: + if isinstance(a, str): + levels[a] = level_dict.get(a, None) + if isinstance(b, str): + levels[b] = level_dict.get(b, None) + levels["Purple upper"] = level_dict.get("Purple upper", float("inf")) + levels["Purple lower"] = level_dict.get("Purple lower", float("-inf")) + + for zone_name, upper_bound, lower_bound in zone_definitions: + upper_value = levels.get(upper_bound, float("inf")) if isinstance(upper_bound, str) else upper_bound + lower_value = levels.get(lower_bound, float("-inf")) if isinstance(lower_bound, str) else lower_bound + if lower_value < price <= upper_value: + return zone_name + return "Unknown" + +# === Main Zone Transition Logic === +def get_zone_transitions_today(): + if datetime.now().weekday() >= 5: + print("⏸ Weekend detected — skipping transition scan") + return pd.DataFrame() + + since = datetime.now(timezone.utc) - timedelta(hours=24) + key_levels_df = load_key_levels(KEY_LEVELS_FILE) + transitions = [] + + for ticker in TICKER_LIST: + print(f"[→] Checking {ticker}...") + try: + data = yf.download(ticker, start=since.strftime('%Y-%m-%d'), interval="1h", progress=False) + if data.empty: + print(f"[âš ī¸] No data for {ticker} — skipping") + continue + data = data.dropna() + + short = ticker.split("=")[0] + "=X" if "=X" in ticker else ticker + levels_series = key_levels_df.loc[short].dropna() + level_dict = dict(levels_series) + + previous_zone = None + for ts, row in data.iterrows(): + price = row['Close'].item() + zone = compute_current_zone(price, ZONE_DEFINITIONS, level_dict) + if previous_zone is not None and zone != previous_zone: + transitions.append({ + "Timestamp": ts, + "Ticker": ticker, + "From Zone": previous_zone, + "To Zone": zone, + "Price": price + }) + previous_zone = zone + + except Exception as e: + print(f"[❌] Failed for {ticker}: {e}") + + df_transitions = pd.DataFrame(transitions) + if not df_transitions.empty: + os.makedirs("reports", exist_ok=True) + log_file = "reports/zone_transition_log.csv" + + # Handle existing file with potentially different column names + if os.path.exists(log_file): + try: + # Read existing file to check its structure + existing_df = pd.read_csv(log_file) + + # If existing file has 'Date' instead of 'Timestamp', rename it + if 'Date' in existing_df.columns and 'Timestamp' not in existing_df.columns: + existing_df = existing_df.rename(columns={'Date': 'Timestamp'}) + # Rewrite the file with standardized column names + existing_df.to_csv(log_file, index=False) + print("[🔄] Standardized existing CSV column names") + + # Now append the new data + df_transitions.to_csv(log_file, mode="a", index=False, header=False) + + except Exception as e: + print(f"[âš ī¸] Issue with existing file, creating backup: {e}") + # Create backup and start fresh + backup_file = log_file.replace('.csv', '_backup.csv') + if os.path.exists(log_file): + os.rename(log_file, backup_file) + df_transitions.to_csv(log_file, index=False) + else: + # New file + df_transitions.to_csv(log_file, index=False) + + print(f"[💾] Zone transitions saved to: {log_file}") + else: + print("[✅] No zone transitions detected today.") + + return df_transitions + +if __name__ == "__main__": + get_zone_transitions_today() diff --git a/archive/unused_data/history b/archive/unused_data/history new file mode 100644 index 0000000..e69de29 diff --git a/archive/unused_data/local_loader.py b/archive/unused_data/local_loader.py new file mode 100644 index 0000000..cc56374 --- /dev/null +++ b/archive/unused_data/local_loader.py @@ -0,0 +1,36 @@ +# data/local_loader.py + +import pandas as pd # data/local_loader.py + +import pandas as pd +import os + +CSV_FOLDER = r"C:\Users\T460\Documents\Quant_trading_research\Quant_framework\data\csv_data" + +def load_local_csv(ticker, start_date=None, end_date=None): + try: + filepath = os.path.join(CSV_FOLDER, f"{ticker}.csv") + print(f"[📄] Loading CSV: {filepath}") + + df = pd.read_csv(filepath, parse_dates=['Date'], decimal=',') + + df.columns = df.columns.str.strip() + df['Date'] = pd.to_datetime(df['Date'], errors='coerce') + df.dropna(subset=['Date'], inplace=True) + df.set_index('Date', inplace=True) + + for col in ['Open', 'High', 'Low', 'Close']: + df[col] = df[col].astype(str).str.replace(',', '.').astype(float) + + print(f"[đŸ“Ĩ] Loaded {ticker}: full range {df.index.min()} to {df.index.max()}") + + if start_date and end_date: + print(f"[📆] Filtering from {start_date} to {end_date}") + df = df.loc[start_date:end_date] + print(f"[✅] After filter: {df.shape[0]} rows") + + return df + + except Exception as e: + print(f"[❌] Failed to load {ticker}.csv: {e}") + return pd.DataFrame() diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/zone_locator.py b/core/zone_locator.py new file mode 100644 index 0000000..f084950 --- /dev/null +++ b/core/zone_locator.py @@ -0,0 +1,168 @@ +import yfinance as yf +import pandas as pd +from datetime import datetime, timedelta, timezone +from pathlib import Path +from collections import defaultdict +import pytz +import smtplib +import os +import json + +from pathlib import Path +BASE_DIR = Path(__file__).resolve().parents[1] # adjust as needed +DATA_DIR = BASE_DIR / "data" +REPORTS_DIR = BASE_DIR / "reports" +REPORTS_DIR.mkdir(exist_ok=True) + +# === CONFIG === +ZONE_STATE_FILE = REPORTS_DIR / "last_known_zone.json" +KEY_LEVELS_FILE = DATA_DIR / "Key_levels_1D.xlsx" +TICKER_LIST = [ + 'GBPNZD=X', 'EURCHF=X', 'NZDCAD=X', 'USDZAR=X', 'CADCHF=X', + 'GBPJPY=X', 'AUDNZD=X', 'GBPCHF=X', 'USDCAD=X', 'CADJPY=X', + 'AUDJPY=X', 'EURUSD=X', 'EURGBP=X', 'USDNOK=X', 'NOKSEK=X' +] +PIP_RANGE = 0.001 +LOOKBACK_HOURS = 24 +since = datetime.now(timezone.utc) - timedelta(hours=LOOKBACK_HOURS) + +ZONE_DEFINITIONS = [ + ("Premium+", float("inf"), "Purple upper"), + ("Premium", "Purple upper", "Red Upper"), + ("Plus+", "Red Upper", "Yellow Upper"), + ("Fair", "Yellow Upper", "Green"), + ("Budget", "Green", "Yellow Lower"), + ("Discount", "Yellow Lower", "Red Lower"), + ("Clearance", "Red Lower", "Purple lower"), + ("Reset", "Purple lower", float("-inf")) +] + +if os.path.exists(ZONE_STATE_FILE): + with open(ZONE_STATE_FILE, "r") as f: + last_known_zone = json.load(f) +else: + last_known_zone = {} + +def load_key_levels(filepath): + df = pd.read_excel(filepath) + df.set_index("Ticker", inplace=True) + return df + +def compute_current_zone(price, zone_definitions, level_dict): + levels = {} + for _, upper_bound, lower_bound in zone_definitions: + if isinstance(upper_bound, str): + levels[upper_bound] = float(level_dict.get(upper_bound, float("inf"))) + if isinstance(lower_bound, str): + levels[lower_bound] = float(level_dict.get(lower_bound, float("-inf"))) + + levels["Purple upper"] = float(level_dict.get("Purple upper", float("inf"))) + levels["Purple lower"] = float(level_dict.get("Purple lower", float("-inf"))) + + for zone_name, upper_bound, lower_bound in zone_definitions: + upper_value = levels.get(upper_bound, float("inf")) if isinstance(upper_bound, str) else upper_bound + lower_value = levels.get(lower_bound, float("-inf")) if isinstance(lower_bound, str) else lower_bound + if lower_value < price <= upper_value: + return zone_name + return "Unknown" + +def generate_current_zone_snapshot(): + key_levels_df = load_key_levels(KEY_LEVELS_FILE) + current_zone_results = [] + + for ticker in TICKER_LIST: + print(f"[→] Checking {ticker} current zone...") + try: + data = yf.download(ticker, period="1d", interval="1h", progress=False) + if data.empty: + print(f"[âš ī¸] No data for {ticker}") + continue + + latest_close = data["Close"].iloc[-1].item() + short = ticker.split("=")[0] + "=X" if "=X" in ticker else ticker + levels_series = key_levels_df.loc[short].dropna() + level_dict = {k: float(v) for k, v in levels_series.items()} + zone = compute_current_zone(latest_close, ZONE_DEFINITIONS, level_dict) + + history_row = { + "Date": pd.Timestamp.utcnow().strftime("%Y-%m-%d"), + "Ticker": ticker, + "Zone": zone + } + history_file = "reports/zone_history.csv" + pd.DataFrame([history_row]).to_csv(history_file, mode="a", index=False, header=not os.path.exists(history_file)) + + previous_zone = last_known_zone.get(ticker, None) + if previous_zone != zone and previous_zone is not None: + transition_row = { + "Date": pd.Timestamp.utcnow().strftime("%Y-%m-%d %H:%M:%S"), + "Ticker": ticker, + "From Zone": previous_zone, + "To Zone": zone + } + transition_file = "reports/zone_transition_log.csv" + pd.DataFrame([transition_row]).to_csv(transition_file, mode="a", index=False, header=not os.path.exists(transition_file)) + last_known_zone[ticker] = zone + + print(f"[✓] {ticker} → Zone: {zone} (Price: {latest_close:.4f})") + current_zone_results.append({ + "Ticker": ticker, + "Current Zone": zone, + "Current Price": latest_close + }) + except Exception as e: + print(f"[❌] Failed for {ticker}: {e}") + + with open(ZONE_STATE_FILE, "w") as f: + json.dump(last_known_zone, f) + + df_current_zones = pd.DataFrame(current_zone_results).sort_values(by="Current Zone") + print("[✅] Current Zone Snapshot:\n") + print(df_current_zones.to_string(index=False)) + + os.makedirs("reports", exist_ok=True) + outpath = "reports/current_zone_snapshot.xlsx" + df_current_zones.to_excel(outpath, index=False) + print(f"[💾] Exported current zone snapshot to: {outpath}") + return df_current_zones + +def export_current_zone_heatmap(df_current_zones, output_path="reports/current_zone_snapshot_heatmap.xlsx"): + if df_current_zones.empty: + print("[âš ī¸] No current zones to export.") + return + + print("[🎨] Exporting color heatmap version...") + with pd.ExcelWriter(output_path, engine="xlsxwriter") as writer: + df_current_zones.to_excel(writer, sheet_name="Current Zones", index=False) + workbook = writer.book + worksheet = writer.sheets["Current Zones"] + + format_premium_plus = workbook.add_format({"bg_color": "#DA70D6"}) # Purple-Gold + format_premium = workbook.add_format({"bg_color": "#FFD700", "bold": True}) # Gold + format_plus = workbook.add_format({"bg_color": "#FFA500"}) # Orange + format_fair = workbook.add_format({"bg_color": "#90EE90"}) # LightGreen + format_budget = workbook.add_format({"bg_color": "#ADD8E6"}) # LightBlue + format_discount = workbook.add_format({"bg_color": "#FF9999"}) # LightRed + format_clearance = workbook.add_format({"bg_color": "#FF5555"}) # Deep Red + format_reset = workbook.add_format({"bg_color": "#A9A9A9"}) # Gray + + + zone_col = df_current_zones.columns.get_loc("Current Zone") + zone_range = f"${chr(65 + zone_col)}2:${chr(65 + zone_col)}{len(df_current_zones)+1}" + + worksheet.conditional_format(zone_range, {"type": "text", "criteria": "containing", "value": "Premium+", "format": format_premium_plus}) + worksheet.conditional_format(zone_range, {"type": "text", "criteria": "containing", "value": "Premium", "format": format_premium}) + worksheet.conditional_format(zone_range, {"type": "text", "criteria": "containing", "value": "Plus+", "format": format_plus}) + worksheet.conditional_format(zone_range, {"type": "text", "criteria": "containing", "value": "Fair", "format": format_fair}) + worksheet.conditional_format(zone_range, {"type": "text", "criteria": "containing", "value": "Budget", "format": format_budget}) + worksheet.conditional_format(zone_range, {"type": "text", "criteria": "containing", "value": "Discount", "format": format_discount}) + worksheet.conditional_format(zone_range, {"type": "text", "criteria": "containing", "value": "Clearance", "format": format_clearance}) + worksheet.conditional_format(zone_range, {"type": "text", "criteria": "containing", "value": "Reset", "format": format_reset}) + + print(f"[💾] Exported color heatmap to: {output_path}") + +if __name__ == "__main__": + df_current_zones = generate_current_zone_snapshot() + export_current_zone_heatmap(df_current_zones) + + \ No newline at end of file diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..303d174 Binary files /dev/null and b/requirements.txt differ diff --git a/scan_x.py b/scan_x.py new file mode 100644 index 0000000..2ac418d --- /dev/null +++ b/scan_x.py @@ -0,0 +1,156 @@ +import streamlit as st + +# App configuration - MUST BE ABSOLUTE FIRST +st.set_page_config( + page_title="QuantFX Analytics Platform", + page_icon="📈", + layout="wide", + initial_sidebar_state="expanded" +) + +# Safe imports with error handling +def safe_import(): + try: + from viz.home import home + return home, None + except Exception as e: + st.error(f"Error importing home: {e}") + return None, str(e) + +def safe_import_fx_heatmap(): + try: + from viz.fx_heatmap import fx_heatmap + return fx_heatmap, None + except Exception as e: + st.error(f"Error importing fx_heatmap: {e}") + return None, str(e) + +def safe_import_zone_locator(): + try: + from viz.zone_locator import zone_locator + return zone_locator, None + except Exception as e: + st.error(f"Error importing zone_locator: {e}") + return None, str(e) + +def safe_import_zone_transitions(): + try: + from viz.zone_transitions import zone_transitions + return zone_transitions, None + except Exception as e: + st.error(f"Error importing zone_transitions: {e}") + return None, str(e) + +def safe_import_strength_meter(): + try: + from viz.strength_meter import strength_meter + return strength_meter, None + except Exception as e: + st.info(f"Strength meter not available: {e}") + return None, str(e) + +def safe_import_fx_correlation(): + try: + from viz.fx_correlation import fx_correlation + return fx_correlation, None + except Exception as e: + st.info(f"FX correlation not available: {e}") + return None, str(e) + +# Import functions safely +home_func, home_error = safe_import() +fx_heatmap_func, fx_error = safe_import_fx_heatmap() +zone_locator_func, zl_error = safe_import_zone_locator() +zone_transitions_func, zt_error = safe_import_zone_transitions() +strength_meter_func, sm_error = safe_import_strength_meter() +fx_correlation_func, fc_error = safe_import_fx_correlation() + +# Placeholder functions for missing components +def placeholder_strength_meter(): + st.title("đŸ’Ē Currency Strength Meter") + st.info("🚧 Strength Meter will be extracted from FX Heatmap soon!") + st.markdown(""" + ### Coming Soon: + - Individual currency strength analysis + - Strength rankings and trends + - Visual strength indicators + """) + +def placeholder_correlation(): + st.title("🔗 Correlation Tool") + st.info("🚧 Correlation analysis tool coming soon!") + st.markdown(""" + ### Planned Features: + - Currency pair correlations + - Correlation heatmaps + - Historical correlation trends + """) + +def placeholder_zone_transitions(): + st.markdown("# 🚧 Zone Transitions") + st.markdown("## 🟡 Coming Soon!") + st.info("We're working on advanced zone transition tracking and alerts.") + + st.markdown("### This feature will include:") + col1, col2 = st.columns(2) + + with col1: + st.markdown("- 📊 **Real-time zone transition alerts**") + st.markdown("- 📈 **Historical transition analysis**") + + with col2: + st.markdown("- 🔔 **Breakout notifications**") + st.markdown("- 📋 **Transition probability scoring**") + + st.markdown("---") + st.info("💡 Check back soon for updates!") + + # Add some current functionality metrics + col1, col2, col3 = st.columns(3) + + with col1: + st.metric("đŸŽ¯ Zone Locator, Correlation Matrix", "✅ Active") + + with col2: + st.metric("📊 FX Heatmap, Strength Meter", "✅ Active") + + with col3: + st.metric("🚧 Zone Transitions", "🔜 Beta") + +# Clean 6-tab structure +TABS = { + "🏠 Home": home_func or (lambda: st.error("Home not available")), + "📊 FX Heatmap": fx_heatmap_func or (lambda: st.error("FX Heatmap not available")), + "đŸ’Ē Strength Meter": strength_meter_func or placeholder_strength_meter, + "📍 Zone Locator(NEW!)": zone_locator_func or (lambda: st.error("Zone Locator not available")), + "🔄 Zone Transitions(NEW!)": placeholder_zone_transitions, + "🔗 Correlation Tool": fx_correlation_func or placeholder_correlation, +} + +# Header +st.title("📈 QuantFX Research & Visualization Platform") +st.markdown("---") + +# Show any import errors in sidebar +if any([home_error, fx_error, zl_error, zt_error]): + with st.sidebar: + st.warning("âš ī¸ Some components failed to load") + if st.checkbox("Show error details"): + if home_error: st.error(f"Home: {home_error}") + if fx_error: st.error(f"FX Heatmap: {fx_error}") + if zl_error: st.error(f"Zone Locator: {zl_error}") + if zt_error: st.error(f"Zone Transitions: {zt_error}") + +# Navigation +selected_tab = st.sidebar.selectbox("🧭 Navigate", list(TABS.keys())) + +# Route to selected function +try: + TABS[selected_tab]() +except Exception as e: + st.error(f"Error loading {selected_tab}: {e}") + st.write("**Debug info:**", str(e)) + + # Show traceback for debugging + import traceback + st.code(traceback.format_exc()) \ No newline at end of file diff --git a/viz/__init__.py b/viz/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/viz/fx_correlation.py b/viz/fx_correlation.py new file mode 100644 index 0000000..c2cdcb9 --- /dev/null +++ b/viz/fx_correlation.py @@ -0,0 +1,435 @@ +# viz/fx_correlation.py +import streamlit as st +import pandas as pd +import numpy as np +import yfinance as yf +from datetime import datetime, timedelta +import plotly.express as px +import plotly.graph_objects as go +from itertools import combinations + +# === CONFIG (Same as your other tools) === +CURRENCY_LIST = ['USD','CAD', 'EUR', 'GBP', 'CHF', 'NOK', 'SGD','JPY', 'AUD', 'NZD'] + +def generate_major_pairs(): + """Generate list of major FX pairs that actually exist in YFinance""" + + # Start with known working major pairs + major_pairs = [ + # USD pairs (these definitely work) + 'EURUSD=X', 'GBPUSD=X', 'AUDUSD=X', 'NZDUSD=X', + 'USDCAD=X', 'USDCHF=X', 'USDJPY=X', 'USDSGD=X', + + # Major crosses (tested to work) + 'EURGBP=X', 'EURJPY=X', 'EURCHF=X', 'EURAUD=X', + 'GBPJPY=X', 'GBPCHF=X', 'GBPAUD=X', + 'AUDJPY=X', 'AUDCAD=X', 'AUDCHF=X', + 'NZDJPY=X', 'NZDCAD=X', 'NZDCHF=X', + 'CADJPY=X', 'CADCHF=X', + 'CHFJPY=X' + ] + + return major_pairs + +def get_historical_data(ticker, days=30): + """Get historical price data for correlation calculation""" + try: + end_date = datetime.now() + start_date = end_date - timedelta(days=days) + + data = yf.download( + ticker, + start=start_date, + end=end_date, + interval="1d", + progress=False, + auto_adjust=False + ) + + if len(data) < 5: # Need minimum data points + return None + + # Calculate daily returns (percentage change) + # Ensure we get a proper Series, not DataFrame + close_prices = data['Close'] + if isinstance(close_prices, pd.DataFrame): + close_prices = close_prices.iloc[:, 0] # Get first column if DataFrame + + returns = close_prices.pct_change().dropna() + + # Verify we have a proper Series + if isinstance(returns, pd.DataFrame): + returns = returns.iloc[:, 0] # Convert to Series if still DataFrame + + return returns + + except Exception as e: + print(f"[âš ī¸] Error fetching {ticker}: {e}") + return None + +def calculate_correlation_matrix(pairs, time_period=30): + """Calculate correlation matrix for all FX pairs""" + + # Progress tracking + progress_bar = st.progress(0) + status_text = st.empty() + + # Dictionary to store returns data + returns_data = {} + failed_pairs = [] + + status_text.text("📊 Fetching historical data...") + + # Fetch data for all pairs + for i, pair in enumerate(pairs): + pair_name = pair.replace('=X', '') + status_text.text(f"Fetching {pair_name}...") + + returns = get_historical_data(pair, time_period) + if returns is not None and len(returns) >= 5: # Need minimum 5 data points + returns_data[pair_name] = returns + print(f"✅ {pair_name}: {len(returns)} data points") + else: + failed_pairs.append(pair_name) + print(f"❌ {pair_name}: Failed or insufficient data") + + progress_bar.progress((i + 1) / len(pairs)) + + # Clear progress indicators + progress_bar.empty() + + # Debug info + st.write(f"**Debug:** Successfully fetched {len(returns_data)} pairs, {len(failed_pairs)} failed") + if failed_pairs: + st.write(f"**Failed pairs:** {', '.join(failed_pairs[:5])}") + + if len(returns_data) < 2: + st.error("❌ Need at least 2 currency pairs with valid data") + return None, None + + status_text.text("🧮 Calculating correlations...") + + # Find common date range across all pairs + common_dates = None + for pair_name, returns in returns_data.items(): + if common_dates is None: + common_dates = returns.index + else: + common_dates = common_dates.intersection(returns.index) + + st.write(f"**Debug:** Found {len(common_dates)} common trading days") + + if len(common_dates) < 5: + st.error("❌ Not enough common trading days across pairs") + return None, None + + # Align all returns to common dates and verify data structure + aligned_returns = {} + for pair_name, returns in returns_data.items(): + aligned_data = returns.loc[common_dates] + if len(aligned_data) > 0 and not aligned_data.empty: + aligned_returns[pair_name] = aligned_data + + st.write(f"**Debug:** {len(aligned_returns)} pairs aligned successfully") + + if len(aligned_returns) < 2: + st.error("❌ Not enough pairs after alignment") + return None, None + + # Create DataFrame with explicit index + try: + returns_df = pd.DataFrame(aligned_returns, index=common_dates) + st.write(f"**Debug:** DataFrame created: {returns_df.shape}") + + # Calculate correlation matrix + correlation_matrix = returns_df.corr() + + # Calculate summary stats + summary_stats = analyze_correlations(correlation_matrix) + + status_text.empty() + + return correlation_matrix, summary_stats + + except Exception as e: + st.error(f"❌ Error creating correlation matrix: {e}") + status_text.empty() + return None, None + +def analyze_correlations(corr_matrix): + """Analyze correlation matrix for trading insights""" + + # Get upper triangle (avoid duplicate pairs) + mask = np.triu(np.ones_like(corr_matrix), k=1).astype(bool) + upper_triangle = corr_matrix.where(mask) + + # Find strongest correlations + correlations_list = [] + for i in range(len(corr_matrix.columns)): + for j in range(i+1, len(corr_matrix.columns)): + pair1 = corr_matrix.columns[i] + pair2 = corr_matrix.columns[j] + corr_value = corr_matrix.iloc[i, j] + + if not pd.isna(corr_value): + correlations_list.append({ + 'Pair_1': pair1, + 'Pair_2': pair2, + 'Correlation': corr_value, + 'Abs_Correlation': abs(corr_value) + }) + + correlations_df = pd.DataFrame(correlations_list) + + if len(correlations_df) == 0: + return None + + # Sort by absolute correlation strength + correlations_df = correlations_df.sort_values('Abs_Correlation', ascending=False) + + return { + 'strongest_positive': correlations_df[correlations_df['Correlation'] > 0].head(5), + 'strongest_negative': correlations_df[correlations_df['Correlation'] < 0].head(5), + 'very_correlated': correlations_df[correlations_df['Abs_Correlation'] >= 0.75], + 'uncorrelated': correlations_df[correlations_df['Abs_Correlation'] <= 0.25].head(5), + 'avg_correlation': correlations_df['Abs_Correlation'].mean() + } + +def create_correlation_heatmap(corr_matrix, time_period): + """Create beautiful correlation heatmap""" + + # Custom colorscale: Red (negative) -> White (neutral) -> Green (positive) + colorscale = [ + [0.0, "#F8696B"], # Strong negative (Red) + [0.25, "#FFB6C1"], # Weak negative (Light Red) + [0.5, "#FFFFFF"], # No correlation (White) + [0.75, "#90EE90"], # Weak positive (Light Green) + [1.0, "#63BE7B"] # Strong positive (Green) + ] + + # Create annotations for correlation values + annotations = [] + for i, row in enumerate(corr_matrix.index): + for j, col in enumerate(corr_matrix.columns): + value = corr_matrix.iloc[i, j] + if not pd.isna(value): + # Color text based on correlation strength for readability + text_color = "white" if abs(value) > 0.6 else "black" + annotations.append( + dict( + x=j, y=i, + text=f"{value:.2f}", + showarrow=False, + font=dict(color=text_color, size=10, family="Arial Black") + ) + ) + + fig = go.Figure(data=go.Heatmap( + z=corr_matrix.values, + x=corr_matrix.columns, + y=corr_matrix.index, + colorscale=colorscale, + zmid=0, # Center the colorscale at 0 + zmin=-1, + zmax=1, + showscale=True, + colorbar=dict( + title="Correlation", + title_font=dict(color="white", size=12), # Updated property name + tickfont=dict(color="white"), + tickmode="array", + tickvals=[-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1], + ticktext=["-1.0", "-0.75", "-0.5", "-0.25", "0", "0.25", "0.5", "0.75", "1.0"] + ), + hoverongaps=False, + hovertemplate='%{y} vs %{x}
Correlation: %{z:.3f}
' + )) + + fig.add_annotation( + text="", + showarrow=False, + x=0, y=0 + ) + + fig.update_layout( + annotations=annotations, + title={ + 'text': f"🔗 FX Pair Correlation Matrix - {time_period} Days", + 'x': 0.5, + 'font': {'size': 18, 'color': 'white', 'family': 'Arial Black'} + }, + xaxis_title="Currency Pairs", + yaxis_title="Currency Pairs", + font=dict(size=10, color='white'), + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + height=800, + margin=dict(l=100, r=100, t=100, b=100) + ) + + # Rotate x-axis labels for better readability + fig.update_xaxes( + tickangle=45, + tickfont=dict(size=10, color='white', family='Arial Black') + ) + fig.update_yaxes( + tickfont=dict(size=10, color='white', family='Arial Black') + ) + + return fig + +def display_correlation_insights(summary_stats): + """Display trading insights from correlation analysis""" + + if summary_stats is None: + return + + st.subheader("📈 Correlation Insights") + + col1, col2 = st.columns(2) + + with col1: + st.markdown("**đŸŸĸ Strongest Positive Correlations**") + if len(summary_stats['strongest_positive']) > 0: + for _, row in summary_stats['strongest_positive'].head(3).iterrows(): + st.write(f"â€ĸ {row['Pair_1']} ↔ {row['Pair_2']}: **{row['Correlation']:.3f}**") + else: + st.write("No strong positive correlations found") + + with col2: + st.markdown("**🔴 Strongest Negative Correlations**") + if len(summary_stats['strongest_negative']) > 0: + for _, row in summary_stats['strongest_negative'].head(3).iterrows(): + st.write(f"â€ĸ {row['Pair_1']} ↔ {row['Pair_2']}: **{row['Correlation']:.3f}**") + else: + st.write("No strong negative correlations found") + + # Very correlated pairs (Âą0.75+) + if len(summary_stats['very_correlated']) > 0: + st.markdown("**⚡ Very High Correlations (Âą0.75+)**") + very_corr_df = summary_stats['very_correlated'].head(5) + st.dataframe( + very_corr_df[['Pair_1', 'Pair_2', 'Correlation']].round(3), + use_container_width=True, + hide_index=True + ) + +def fx_correlation(): + st.title("🔗 FX Pair Correlation Analysis") + + # Time period selector + col1, col2, col3 = st.columns([1, 2, 1]) + + with col1: + time_period = st.selectbox( + "📅 Time Period", + [7, 14, 30, 60, 90], + index=2, # Default to 30 days + help="Number of days for correlation calculation" + ) + + with col2: + refresh_data = st.button("🔄 Refresh Correlation Data", help="Recalculate correlations") + + with col3: + st.info(f"🕐 {datetime.now().strftime('%H:%M UTC')}") + + # Generate pairs list + pairs_list = generate_major_pairs() + + # Cache key based on time period + cache_key = f'correlation_cache_{time_period}d' + timestamp_key = f'correlation_timestamp_{time_period}d' + + # Generate or use cached data + if refresh_data or cache_key not in st.session_state: + st.info(f"🚀 Calculating {time_period}-day correlations...") + + with st.spinner("Analyzing currency pair relationships..."): + correlation_matrix, summary_stats = calculate_correlation_matrix(pairs_list, time_period) + + if correlation_matrix is not None: + st.session_state[cache_key] = (correlation_matrix, summary_stats) + st.session_state[timestamp_key] = datetime.now() + else: + st.error("❌ Could not calculate correlations - insufficient data") + return + else: + correlation_matrix, summary_stats = st.session_state[cache_key] + cache_time = st.session_state.get(timestamp_key, datetime.now()) + st.caption(f"📋 Cached data from: {cache_time.strftime('%H:%M:%S')}") + + # Display results + if correlation_matrix is not None: + # Main correlation heatmap + fig = create_correlation_heatmap(correlation_matrix, time_period) + st.plotly_chart(fig, use_container_width=True) + + # Display insights + display_correlation_insights(summary_stats) + + # Summary statistics + st.subheader("📊 Market Overview") + col1, col2, col3, col4 = st.columns(4) + + if summary_stats: + with col1: + avg_corr = summary_stats['avg_correlation'] + st.metric("📈 Avg Correlation", f"{avg_corr:.3f}") + + with col2: + very_corr_count = len(summary_stats['very_correlated']) + st.metric("⚡ Very Correlated", f"{very_corr_count} pairs") + + with col3: + uncorr_count = len(summary_stats['uncorrelated']) + st.metric("âžĄī¸ Uncorrelated", f"{uncorr_count} pairs") + + with col4: + total_pairs = len(correlation_matrix.columns) * (len(correlation_matrix.columns) - 1) // 2 + st.metric("đŸ”ĸ Total Pairs", f"{total_pairs}") + + # Export functionality + st.subheader("💾 Export Data") + col1, col2 = st.columns(2) + + with col1: + csv_data = correlation_matrix.to_csv().encode('utf-8') + st.download_button( + "đŸ“Ĩ Download Correlation Matrix", + csv_data, + file_name=f"fx_correlation_{time_period}d_{datetime.now().strftime('%Y%m%d_%H%M')}.csv", + mime="text/csv" + ) + + with col2: + if st.checkbox("📋 Show Raw Data"): + st.dataframe( + correlation_matrix.round(3).style.background_gradient( + cmap="RdYlGn", + axis=None, + vmin=-1, + vmax=1 + ), + use_container_width=True + ) + + # Educational info + st.markdown("---") + st.markdown(""" + **📖 Understanding FX Correlations:** + + **đŸŸĸ Positive Correlation (+0.75 to +1.0):** Pairs move in same direction + - *Example: EURUSD & GBPUSD often rise/fall together* + + **🔴 Negative Correlation (-0.75 to -1.0):** Pairs move in opposite directions + - *Example: EURUSD & USDCHF typically move inversely* + + **âšĒ No Correlation (Âą0.25):** Pairs move independently + - *Good for portfolio diversification* + + **💡 Trading Applications:** + - **Risk Management:** Avoid taking multiple positions in highly correlated pairs + - **Hedging:** Use negatively correlated pairs to offset risk + - **Confirmation:** Strong correlations can confirm trade signals + """) \ No newline at end of file diff --git a/viz/fx_heatmap.py b/viz/fx_heatmap.py new file mode 100644 index 0000000..df5a9ff --- /dev/null +++ b/viz/fx_heatmap.py @@ -0,0 +1,192 @@ +# viz/fx_heatmap.py +# viz/fx_heatmap.py +import streamlit as st +import pandas as pd +import numpy as np +import yfinance as yf +from datetime import datetime, timezone +import plotly.express as px +import plotly.graph_objects as go + +# === CONFIG === +CURRENCY_LIST = ['USD','CAD', 'EUR', 'GBP', 'CHF','SGD','JPY', 'AUD', 'NZD'] + +def get_daily_pct_change(ticker): + """Get daily percentage change for a currency pair""" + try: + data = yf.download(ticker, period="2d", interval="1d", progress=False, auto_adjust=False) + if len(data) < 2: + return None + open_val = data['Open'].iloc[-1].item() + close_val = data['Close'].iloc[-1].item() + return (close_val - open_val) / open_val * 100 + except Exception as e: + print(f"[âš ī¸] Error fetching {ticker}: {e}") + return None + +def generate_live_heatmap(): + """Generate live FX percentage change heatmap""" + matrix = pd.DataFrame(index=CURRENCY_LIST, columns=CURRENCY_LIST, dtype=float) + + # Progress bar for data fetching + progress_bar = st.progress(0) + status_text = st.empty() + + total_pairs = len(CURRENCY_LIST) * (len(CURRENCY_LIST) - 1) + current_pair = 0 + + for base in CURRENCY_LIST: + for quote in CURRENCY_LIST: + if base == quote: + matrix.at[base, quote] = 0.0 # Same currency = 0% + continue + + pair = f"{base}{quote}=X" + status_text.text(f"Fetching {pair}...") + + pct_change = get_daily_pct_change(pair) + if pct_change is not None: + matrix.at[base, quote] = round(pct_change, 2) + else: + matrix.at[base, quote] = np.nan + + current_pair += 1 + progress_bar.progress(current_pair / total_pairs) + + # Clear progress indicators + progress_bar.empty() + status_text.empty() + + return matrix + +def create_beautiful_heatmap(matrix): + """Create a beautiful plotly heatmap with your preferred styling""" + + # Create custom colorscale (Green -> Yellow -> Red) + colorscale = [ + [0.0, "#63BE7B"], # Green (negative/good for some pairs) + [0.5, "#FFEB84"], # Yellow (neutral) + [1.0, "#F8696B"] # Red (positive/bad for some pairs) + ] + + fig = go.Figure(data=go.Heatmap( + z=matrix.values, + x=matrix.columns, + y=matrix.index, + colorscale=colorscale, + showscale=True, + text=matrix.values, + texttemplate="%{text:.2f}%", + textfont={"size": 12, "color": "black", "family": "Arial Black"}, + hoverongaps=False, + hovertemplate='%{y}/%{x}
Change: %{z:.2f}%' + )) + + fig.update_layout( + title={ + 'text': f"FX Daily % Change Heatmap - {datetime.now().strftime('%Y-%m-%d')}", + 'x': 0.5, + 'y': 0.95, # Move title up slightly + 'font': {'size': 18, 'color': 'white', 'family': 'Arial Black'} + }, + xaxis_title={ + 'text': "Quote Currency", + 'font': {'size': 14, 'color': 'white', 'family': 'Arial Black'} + }, + yaxis_title={ + 'text': "Base Currency", + 'font': {'size': 14, 'color': 'white', 'family': 'Arial Black'} + }, + font=dict(size=12, color='white'), + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + height=650, # Increased height to give more room + margin=dict(l=100, r=80, t=120, b=80) # Increased top and left margins +) + + fig.update_xaxes( + side="top", + tickfont=dict(size=12, color='white', family='Arial Black'), + title_standoff=20 # Add space between title and ticks +) + fig.update_yaxes( + tickfont=dict(size=12, color='white', family='Arial Black'), + title_standoff=20 # Add space between title and ticks +) + + return fig + +def fx_heatmap(): + st.title("📊 FX Daily % Change Heatmap") + + # Add refresh button and info + col1, col2, col3 = st.columns([1, 2, 1]) + + with col1: + refresh_data = st.button("🔄 Refresh Live Data", help="Fetch latest FX data") + + with col3: + st.info(f"🕐 {datetime.now().strftime('%H:%M UTC')}") + + # Generate or use cached data + if refresh_data or 'fx_heatmap_cache' not in st.session_state: + st.info("🚀 Fetching live FX data...") + + with st.spinner("Loading currency data..."): + matrix = generate_live_heatmap() + st.session_state.fx_heatmap_cache = matrix + st.session_state.heatmap_timestamp = datetime.now() + else: + matrix = st.session_state.fx_heatmap_cache + cache_time = st.session_state.get('heatmap_timestamp', datetime.now()) + st.caption(f"📋 Cached data from: {cache_time.strftime('%H:%M:%S')}") + + # Create and display beautiful heatmap + if not matrix.empty: + fig = create_beautiful_heatmap(matrix) + st.plotly_chart(fig, use_container_width=True) + + # Summary stats + st.subheader("📈 Market Summary") + col1, col2, col3, col4 = st.columns(4) + + # Calculate stats (excluding NaN and zeros) + clean_data = matrix.replace([np.inf, -np.inf], np.nan).dropna().values.flatten() + clean_data = clean_data[clean_data != 0] # Remove diagonal zeros + + if len(clean_data) > 0: + with col1: + st.metric("📊 Strongest Move", f"{clean_data.max():.2f}%") + with col2: + st.metric("📉 Weakest Move", f"{clean_data.min():.2f}%") + with col3: + st.metric("📈 Average Move", f"{clean_data.mean():.2f}%") + with col4: + volatility = clean_data.std() + st.metric("⚡ Volatility", f"{volatility:.2f}%") + + # Export functionality + st.subheader("💾 Export Data") + col1, col2 = st.columns(2) + + with col1: + csv_data = matrix.to_csv().encode('utf-8') + st.download_button( + "đŸ“Ĩ Download CSV", + csv_data, + file_name=f"fx_heatmap_{datetime.now().strftime('%Y%m%d_%H%M')}.csv", + mime="text/csv" + ) + + with col2: + # Show data table + if st.checkbox("📋 Show Raw Data"): + st.dataframe( + matrix.style.background_gradient( + cmap="RdYlGn_r", + axis=None + ).format("{:.2f}%"), + use_container_width=True + ) + else: + st.error("❌ Could not generate heatmap - no data available") \ No newline at end of file diff --git a/viz/home.py b/viz/home.py new file mode 100644 index 0000000..d7ccf91 --- /dev/null +++ b/viz/home.py @@ -0,0 +1,213 @@ +import streamlit as st +import plotly.graph_objects as go +from plotly.subplots import make_subplots +import pandas as pd + +def home(): + # Note: Page config is handled by main app (scan_x.py) + + # Custom CSS for professional styling + st.markdown(""" + + """, unsafe_allow_html=True) + + # Header Section + st.markdown('

FX Quant Scan

', unsafe_allow_html=True) + st.markdown('

Professional FX Analytics & Zone-Based Market Analysis Platform

', unsafe_allow_html=True) + + # Quick Stats Row + col1, col2, col3, col4 = st.columns(4) + with col1: + st.markdown('

28

Currency Pairs

', unsafe_allow_html=True) + with col2: + st.markdown('

6

Analysis Tools

', unsafe_allow_html=True) + with col3: + st.markdown('

Real-time

Market Data

', unsafe_allow_html=True) + with col4: + st.markdown('

Zone-Based

Price Analysis

', unsafe_allow_html=True) + + st.markdown("---") + + # Platform Features + st.markdown("## 🚀 Platform Features") + + features = [ + { + "title": "📊 FX Heatmap", + "description": "Real-time percentage change visualization across 28 major currency pairs. Instantly identify market movers and currency strength patterns.", + "use_case": "Perfect for: Market overview, daily trading preparation, currency strength comparison" + }, + { + "title": "đŸ’Ē Currency Strength Meter", + "description": "Individual currency strength rankings with beautiful interactive charts. Track which currencies are gaining or losing momentum.", + "use_case": "Perfect for: Currency selection, strength-based trading strategies, market sentiment analysis" + }, + { + "title": "đŸŽ¯ Zone Locator", + "description": "Advanced price analysis using historically significant support/resistance levels. Determine if currencies are cheap, fair-priced, or expensive.", + "use_case": "Perfect for: Entry/exit timing, risk assessment, mean reversion strategies" + }, + { + "title": "🔗 FX Correlation Tool", + "description": "Comprehensive 24x24 correlation matrix with multiple timeframes (7-90 days). Understand currency pair relationships and portfolio risk.", + "use_case": "Perfect for: Risk management, portfolio diversification, pair trading strategies" + } + ] + + for feature in features: + st.markdown(f""" +
+

{feature['title']}

+

{feature['description']}

+

{feature['use_case']}

+
+ """, unsafe_allow_html=True) + + st.markdown("---") + + # Zone Analysis Explanation + st.markdown("## đŸŽ¯ Understanding Zone Analysis") + + st.markdown(""" +
+

💡 What Are Trading Zones?

+

Zones are price ranges defined by historical key support and resistance levels. Think of them as "neighborhoods" where currency pairs like to spend time.

+
+ """, unsafe_allow_html=True) + + st.markdown("### 🔍 How It Works:") + st.markdown(""" + - **Historical Analysis:** We analyze years of price data to identify significant support/resistance levels + - **Zone Classification:** Price ranges between these levels become named zones + - **Fair Value Discovery:** By tracking which zones pairs spend most time in, we identify "fair prices" + - **Market Positioning:** Current price location tells us if a currency is cheap, fairly priced, or expensive + """) + + # Zone Categories + st.markdown("### 📊 Zone Categories Explained") + + col1, col2, col3 = st.columns(3) + + with col1: + st.markdown(""" +
+

🔴 RESET Zone

+

Worst Case Scenario

+

Currency pair is seeing new historical lows. Potential oversold conditions, but also possible fundamental deterioration.

+
+ """, unsafe_allow_html=True) + + with col2: + st.markdown(""" +
+

đŸŸĸ BUDGET Zone

+

Fair Value Range

+

Currency pair is trading around historical average levels. This represents "normal" or "fair" pricing based on historical patterns.

+
+ """, unsafe_allow_html=True) + + with col3: + st.markdown(""" +
+

🟠 PREMIUM+ Zone

+

Expensive Territory

+

Currency pair is at historically high levels. May indicate overbought conditions or strong fundamental drivers.

+
+ """, unsafe_allow_html=True) + + st.markdown("---") + + # Quick Start Guide + st.markdown("## 🚀 Quick Start Guide") + + st.markdown(""" + 1. **📊 Start with FX Heatmap** - Get overall market view and identify active currency pairs + 2. **đŸ’Ē Check Currency Strength** - Determine which individual currencies are moving + 3. **đŸŽ¯ Use Zone Locator** - Assess if your target pairs are cheap, fair, or expensive + 4. **🔗 Verify with Correlations** - Ensure your trades aren't highly correlated (risk management) + + **💡 Pro Tip:** Combine zone analysis with currency strength for powerful entry signals - look for strong currencies in budget zones! + """) + + # Footer + st.markdown("---") + st.markdown(""" +
+

FX Quant Scan - Professional FX Analytics Platform

+

Built for traders who value data-driven decisions and quantitative analysis

+
+ """, unsafe_allow_html=True) + +if __name__ == "__main__": + home() \ No newline at end of file diff --git a/viz/strength_meter.py b/viz/strength_meter.py new file mode 100644 index 0000000..cf65302 --- /dev/null +++ b/viz/strength_meter.py @@ -0,0 +1,289 @@ +# viz/strength_meter.py +# viz/strength_meter.py +import streamlit as st +import pandas as pd +import numpy as np +import yfinance as yf +from datetime import datetime, timezone +import plotly.express as px +import plotly.graph_objects as go + +# === CONFIG (Same as your heatmap) === +CURRENCY_LIST = ['USD','CAD', 'EUR', 'GBP', 'CHF', 'NOK', 'SGD','JPY', 'AUD', 'NZD'] + +def get_daily_pct_change(ticker): + """Get daily percentage change for a currency pair (same as heatmap)""" + try: + data = yf.download(ticker, period="2d", interval="1d", progress=False, auto_adjust=False) + if len(data) < 2: + return None + open_val = data['Open'].iloc[-1].item() + close_val = data['Close'].iloc[-1].item() + return (close_val - open_val) / open_val * 100 + except Exception as e: + print(f"[âš ī¸] Error fetching {ticker}: {e}") + return None + +def calculate_currency_strength(): + """Calculate individual currency strength by averaging against all pairs""" + + # Dictionary to store each currency's performance against others + currency_scores = {currency: [] for currency in CURRENCY_LIST} + + # Progress bar for data fetching + progress_bar = st.progress(0) + status_text = st.empty() + + total_pairs = len(CURRENCY_LIST) * (len(CURRENCY_LIST) - 1) + current_pair = 0 + + for base in CURRENCY_LIST: + for quote in CURRENCY_LIST: + if base == quote: + continue # Skip same currency pairs + + pair = f"{base}{quote}=X" + status_text.text(f"Analyzing {pair}...") + + pct_change = get_daily_pct_change(pair) + + if pct_change is not None: + # Base currency gains strength when pair goes UP + currency_scores[base].append(pct_change) + # Quote currency gains strength when pair goes DOWN + currency_scores[quote].append(-pct_change) + + current_pair += 1 + progress_bar.progress(current_pair / total_pairs) + + # Clear progress indicators + progress_bar.empty() + status_text.empty() + + # Calculate average strength for each currency + strength_data = [] + for currency, scores in currency_scores.items(): + if scores: # Only if we have data + avg_strength = np.mean(scores) + strength_data.append({ + 'Currency': currency, + 'Strength_Score': round(avg_strength, 3), + 'Data_Points': len(scores) + }) + + # Convert to DataFrame and sort by strength + df = pd.DataFrame(strength_data) + df = df.sort_values('Strength_Score', ascending=False).reset_index(drop=True) + df['Rank'] = df.index + 1 + + return df + +def create_strength_chart(df): + """Create beautiful strength meter visualization""" + + # Create colors based on strength (Green = Strong, Red = Weak) + colors = [] + for score in df['Strength_Score']: + if score > 0.5: + colors.append('#63BE7B') # Strong Green + elif score > 0.1: + colors.append('#90EE90') # Light Green + elif score > -0.1: + colors.append('#FFEB84') # Yellow (Neutral) + elif score > -0.5: + colors.append('#FFB6C1') # Light Red + else: + colors.append('#F8696B') # Strong Red + + # Create horizontal bar chart + fig = go.Figure() + + fig.add_trace(go.Bar( + y=df['Currency'], + x=df['Strength_Score'], + orientation='h', + marker=dict( + color=colors, + line=dict(color='rgba(0,0,0,0.8)', width=1) + ), + text=[f"{score:.2f}%" for score in df['Strength_Score']], + textposition='outside', + textfont=dict(size=12, color='white', family='Arial Black'), + hovertemplate='%{y}
Strength: %{x:.2f}%
Rank: #%{customdata}', + customdata=df['Rank'] + )) + + fig.update_layout( + title={ + 'text': f"đŸ’Ē Currency Strength Meter - {datetime.now().strftime('%Y-%m-%d')}", + 'x': 0.5, + 'font': {'size': 20, 'color': 'white', 'family': 'Arial Black'} + }, + xaxis_title="Strength Score (%)", + yaxis_title="Currency", + font=dict(size=12, color='white'), + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + height=600, + margin=dict(l=80, r=120, t=100, b=80), + showlegend=False + ) + + # Add vertical line at zero + fig.add_vline(x=0, line_dash="dash", line_color="gray", opacity=0.7) + + fig.update_xaxes(tickfont=dict(size=12, color='white')) + fig.update_yaxes(tickfont=dict(size=12, color='white', family='Arial Black')) + + return fig + +def create_strength_table(df): + """Create a formatted strength ranking table""" + + # Add visual indicators + df_display = df.copy() + + # Add emoji indicators based on strength + def get_strength_emoji(score): + if score > 0.5: + return "🚀" + elif score > 0.1: + return "📈" + elif score > -0.1: + return "âžĄī¸" + elif score > -0.5: + return "📉" + else: + return "đŸ”ģ" + + df_display['Status'] = df_display['Strength_Score'].apply(get_strength_emoji) + df_display['Strength %'] = df_display['Strength_Score'].apply(lambda x: f"{x:.2f}%") + + # Select columns for display + display_df = df_display[['Rank', 'Currency', 'Status', 'Strength %', 'Data_Points']] + + return display_df + +def strength_meter(): + st.title("đŸ’Ē Currency Strength Meter") + + # Add refresh button and info (same pattern as heatmap) + col1, col2, col3 = st.columns([1, 2, 1]) + + with col1: + refresh_data = st.button("🔄 Refresh Live Data", help="Calculate latest currency strength") + + with col3: + st.info(f"🕐 {datetime.now().strftime('%H:%M UTC')}") + + # Generate or use cached data + if refresh_data or 'strength_meter_cache' not in st.session_state: + st.info("🚀 Calculating currency strength...") + + with st.spinner("Analyzing currency pairs..."): + strength_df = calculate_currency_strength() + st.session_state.strength_meter_cache = strength_df + st.session_state.strength_timestamp = datetime.now() + else: + strength_df = st.session_state.strength_meter_cache + cache_time = st.session_state.get('strength_timestamp', datetime.now()) + st.caption(f"📋 Cached data from: {cache_time.strftime('%H:%M:%S')}") + + # Display results + if not strength_df.empty: + # Main strength chart + fig = create_strength_chart(strength_df) + st.plotly_chart(fig, use_container_width=True) + + # Show ranking table + st.subheader("🏆 Currency Rankings") + display_df = create_strength_table(strength_df) + + # Use columns to make it look nicer + col1, col2 = st.columns([2, 1]) + + with col1: + st.dataframe( + display_df.style.apply( + lambda x: ['background-color: #63BE7B; color: black' if i < 3 + else 'background-color: #F8696B; color: white' if i >= len(x) - 3 + else '' for i in range(len(x))], + axis=0 + ), + use_container_width=True, + hide_index=True + ) + + with col2: + st.info(""" + **💡 How to Read:** + + 🚀 **Very Strong** (>0.5%) + 📈 **Strong** (>0.1%) + âžĄī¸ **Neutral** (-0.1% to 0.1%) + 📉 **Weak** (<-0.1%) + đŸ”ģ **Very Weak** (<-0.5%) + """) + + # Summary stats + st.subheader("📊 Market Summary") + col1, col2, col3, col4 = st.columns(4) + + with col1: + strongest = strength_df.iloc[0] + st.metric( + "đŸĨ‡ Strongest", + strongest['Currency'], + f"{strongest['Strength_Score']:.2f}%" + ) + + with col2: + weakest = strength_df.iloc[-1] + st.metric( + "đŸĨ‰ Weakest", + weakest['Currency'], + f"{weakest['Strength_Score']:.2f}%" + ) + + with col3: + avg_strength = strength_df['Strength_Score'].mean() + st.metric("📈 Average", f"{avg_strength:.2f}%") + + with col4: + strength_range = strength_df['Strength_Score'].max() - strength_df['Strength_Score'].min() + st.metric("📏 Range", f"{strength_range:.2f}%") + + # Export functionality + st.subheader("💾 Export Data") + col1, col2 = st.columns(2) + + with col1: + csv_data = strength_df.to_csv(index=False).encode('utf-8') + st.download_button( + "đŸ“Ĩ Download CSV", + csv_data, + file_name=f"currency_strength_{datetime.now().strftime('%Y%m%d_%H%M')}.csv", + mime="text/csv" + ) + + with col2: + # Show raw data + if st.checkbox("📋 Show Raw Data"): + st.dataframe(strength_df, use_container_width=True) + + else: + st.error("❌ Could not calculate currency strength - no data available") + + # Additional info + st.markdown("---") + st.markdown(""" + **📖 About Currency Strength:** + + The strength meter calculates how each currency performs against all other major currencies. + A positive score means the currency is gaining strength, while negative means it's weakening. + + **📊 Calculation Method:** + - For each currency pair (e.g., EURUSD), if EUR goes up, EUR gets +points and USD gets -points + - Each currency's final score is the average of all its pair performances + - Rankings show relative strength in the current market session + """) \ No newline at end of file diff --git a/viz/zone_locator.py b/viz/zone_locator.py new file mode 100644 index 0000000..97ba580 --- /dev/null +++ b/viz/zone_locator.py @@ -0,0 +1,386 @@ +import streamlit as st +import pandas as pd +import plotly.graph_objects as go +import plotly.express as px +import yfinance as yf +from datetime import datetime, timedelta +import sys +import os + +# Import your existing zone locator function +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'core')) +from zone_locator import generate_current_zone_snapshot, TICKER_LIST, ZONE_DEFINITIONS + +def zone_locator(): + # Custom CSS for enhanced styling + st.markdown(""" + + """, unsafe_allow_html=True) + + # Header + st.markdown(""" +
+

đŸŽ¯ Zone Locator

+

Identify if currency pairs are cheap, fairly priced, or expensive based on historical zones

+
+ """, unsafe_allow_html=True) + + # Control Panel + st.markdown("## âš™ī¸ Analysis Controls") + + col1, col2, col3 = st.columns([2, 2, 1]) + + with col1: + selected_pair = st.selectbox( + "đŸŽ¯ Select Currency Pair", + TICKER_LIST, + help="Choose a currency pair to analyze its current zone position" + ) + + with col2: + analysis_period = st.selectbox( + "📅 Analysis Period", + ["1 Month", "3 Months", "6 Months", "1 Year"], + index=2, + help="Historical period for zone context visualization" + ) + + with col3: + refresh_data = st.button("🔄 Refresh Data", type="primary") + + # Zone Legend + st.markdown("### 📊 Zone Classification Legend") + + zone_info = { + 'Premium+': {'color': '#DA70D6', 'desc': 'Extreme highs, very expensive'}, + 'Premium': {'color': '#FFD700', 'desc': 'Historical highs, expensive'}, + 'Plus+': {'color': '#FFA500', 'desc': 'Above fair value'}, + 'Fair': {'color': '#90EE90', 'desc': 'Balanced pricing'}, + 'Budget': {'color': '#ADD8E6', 'desc': 'Good value territory'}, + 'Discount': {'color': '#FF9999', 'desc': 'Below fair value'}, + 'Clearance': {'color': '#FF5555', 'desc': 'Very cheap levels'}, + 'Reset': {'color': '#A9A9A9', 'desc': 'Historical lows, extreme'} + } + + cols = st.columns(4) + for i, (zone, info) in enumerate(zone_info.items()): + with cols[i % 4]: + st.markdown(f""" +
+
+ {zone}
+ {info['desc']} +
+ """, unsafe_allow_html=True) + + st.markdown("---") + + # Get zone data + try: + with st.spinner("🔍 Loading zone data..."): + if refresh_data or 'zone_data' not in st.session_state: + zone_df = generate_current_zone_snapshot() + st.session_state.zone_data = zone_df + else: + zone_df = st.session_state.zone_data + + if zone_df.empty: + st.warning("âš ī¸ No zone data available") + return + + except Exception as e: + st.error(f"❌ Error loading zone data: {str(e)}") + return + + # Find selected pair data + selected_data = zone_df[zone_df['Ticker'] == selected_pair] + + if selected_data.empty: + st.warning(f"âš ī¸ No data available for {selected_pair}") + return + + current_zone = selected_data.iloc[0]['Current Zone'] + current_price = selected_data.iloc[0]['Current Price'] + + # Zone color mapping + zone_colors = { + 'Premium+': '#DA70D6', + 'Premium': '#FFD700', + 'Plus+': '#FFA500', + 'Fair': '#90EE90', + 'Budget': '#ADD8E6', + 'Discount': '#FF9999', + 'Clearance': '#FF5555', + 'Reset': '#A9A9A9' + } + + zone_classes = { + 'Premium+': 'zone-premium-plus', + 'Premium': 'zone-premium', + 'Plus+': 'zone-plus', + 'Fair': 'zone-fair', + 'Budget': 'zone-budget', + 'Discount': 'zone-discount', + 'Clearance': 'zone-clearance', + 'Reset': 'zone-reset' + } + + zone_color = zone_colors.get(current_zone, '#6b7280') + zone_class = zone_classes.get(current_zone, 'zone-neutral') + + # Current Zone Analysis + st.markdown("## 📍 Current Zone Analysis") + + col1, col2 = st.columns([1, 2]) + + with col1: + st.markdown(f""" +
+
Current Zone
+
+ {current_zone} +
+
{current_price:.5f}
+
+ Updated: {datetime.now().strftime('%H:%M:%S')} +
+
+ """, unsafe_allow_html=True) + + with col2: + # Zone distribution chart + zone_counts = zone_df['Current Zone'].value_counts() + + fig_dist = go.Figure(data=[ + go.Bar( + x=zone_counts.index, + y=zone_counts.values, + marker_color=[zone_colors.get(zone, '#6b7280') for zone in zone_counts.index], + text=zone_counts.values, + textposition='auto', + ) + ]) + + fig_dist.update_layout( + title="Current Zone Distribution Across All Pairs", + xaxis_title="Zone", + yaxis_title="Number of Pairs", + height=300, + template="plotly_white" + ) + + st.plotly_chart(fig_dist, use_container_width=True) + + # Zone interpretation + zone_interpretations = { + 'Reset': { + 'emoji': 'âšĒ', + 'title': 'RESET Zone - Extreme Oversold', + 'description': 'This currency pair is at historical lows. Maximum risk/reward potential.', + 'strategy': 'Consider: Contrarian plays, small position sizing, wait for confirmation', + 'risk': 'Very High - New lows possible, fundamental deterioration likely' + }, + 'Clearance': { + 'emoji': '🔴', + 'title': 'CLEARANCE Zone - Very Cheap', + 'description': 'Significantly below normal levels. Strong oversold conditions.', + 'strategy': 'Consider: Value plays, gradual accumulation, support levels', + 'risk': 'High - Further decline possible, but good risk/reward' + }, + 'Discount': { + 'emoji': '🟡', + 'title': 'DISCOUNT Zone - Below Fair Value', + 'description': 'Trading below historical average. Good value territory.', + 'strategy': 'Consider: Buying opportunities, normal position sizing', + 'risk': 'Medium - Normal volatility, favorable entry levels' + }, + 'Budget': { + 'emoji': 'đŸ”ĩ', + 'title': 'BUDGET Zone - Good Value', + 'description': 'Attractive pricing with room for upside to fair value.', + 'strategy': 'Consider: Long positions, trend following, value plays', + 'risk': 'Low-Medium - Good risk/reward balance' + }, + 'Fair': { + 'emoji': 'đŸŸĸ', + 'title': 'FAIR Zone - Balanced Pricing', + 'description': 'Trading around historical average levels. Neutral valuation.', + 'strategy': 'Consider: Momentum strategies, breakout plays, trend following', + 'risk': 'Medium - Normal volatility expected' + }, + 'Plus+': { + 'emoji': '🟠', + 'title': 'PLUS+ Zone - Above Fair Value', + 'description': 'Trading above normal levels. Momentum or early overvaluation.', + 'strategy': 'Consider: Momentum continuation, reduced position sizing', + 'risk': 'Medium-High - Correction risk increasing' + }, + 'Premium': { + 'emoji': '🟡', + 'title': 'PREMIUM Zone - Expensive Territory', + 'description': 'At historically high levels. Strong momentum or overvaluation.', + 'strategy': 'Consider: Trend continuation, tight stops, take profits', + 'risk': 'High - Significant correction risk' + }, + 'Premium+': { + 'emoji': 'đŸŸŖ', + 'title': 'PREMIUM+ Zone - Extreme Highs', + 'description': 'At extreme historical levels. Maximum overvaluation risk.', + 'strategy': 'Consider: Short opportunities, minimal long exposure', + 'risk': 'Very High - Major correction likely' + } + } + + interpretation = zone_interpretations.get(current_zone, zone_interpretations['Fair']) + + st.markdown(f""" +
+

{interpretation['emoji']} {interpretation['title']}

+

{interpretation['description']}

+

Strategy Considerations: {interpretation['strategy']}

+

Risk Level: {interpretation['risk']}

+
+ """, unsafe_allow_html=True) + + st.markdown("---") + + # Historical Price Chart + st.markdown("## 📈 Price Chart with Zone Context") + + try: + period_map = { + "1 Month": "1mo", + "3 Months": "3mo", + "6 Months": "6mo", + "1 Year": "1y" + } + + ticker = yf.Ticker(selected_pair) + hist_data = ticker.history(period=period_map[analysis_period]) + + if not hist_data.empty: + # Create candlestick chart + fig = go.Figure() + + fig.add_trace(go.Candlestick( + x=hist_data.index, + open=hist_data['Open'], + high=hist_data['High'], + low=hist_data['Low'], + close=hist_data['Close'], + name=selected_pair, + increasing_line_color='#059669', + decreasing_line_color='#dc2626' + )) + + # Add current price line + fig.add_hline( + y=current_price, + line_dash="dash", + line_color=zone_color, + annotation_text=f"Current: {current_price:.5f} ({current_zone})" + ) + + fig.update_layout( + title=f"{selected_pair} - Current Zone: {current_zone}", + xaxis_title="Date", + yaxis_title="Price", + height=500, + template="plotly_white", + showlegend=False + ) + + st.plotly_chart(fig, use_container_width=True) + else: + st.warning("âš ī¸ No historical data available for chart") + + except Exception as e: + st.error(f"❌ Error creating chart: {str(e)}") + + # All Zones Summary + st.markdown("## 📊 All Pairs Zone Summary") + + # Style the dataframe + styled_df = zone_df.copy() + styled_df['Current Price'] = styled_df['Current Price'].round(5) + + st.dataframe( + styled_df, + use_container_width=True, + column_config={ + "Ticker": st.column_config.TextColumn("Currency Pair", width="medium"), + "Current Zone": st.column_config.TextColumn("Zone", width="medium"), + "Current Price": st.column_config.NumberColumn("Price", width="medium", format="%.5f") + } + ) + + # Zone Statistics + col1, col2, col3 = st.columns(3) + + with col1: + expensive_zones = zone_df[zone_df['Current Zone'].isin(['Premium+', 'Premium', 'Plus+'])].shape[0] + st.metric("🔴 Expensive Pairs", expensive_zones) + + with col2: + fair_zones = zone_df[zone_df['Current Zone'].isin(['Fair', 'Budget'])].shape[0] + st.metric("đŸŸĸ Fair Value Pairs", fair_zones) + + with col3: + cheap_zones = zone_df[zone_df['Current Zone'].isin(['Discount', 'Clearance', 'Reset'])].shape[0] + st.metric("đŸ”ĩ Cheap Pairs", cheap_zones) + + # Footer + st.markdown("---") + st.markdown(""" +
+

Zone Locator - Historical zone analysis for informed trading decisions

+
+ """, unsafe_allow_html=True) + +if __name__ == "__main__": + zone_locator() \ No newline at end of file diff --git a/viz/zone_transitions.py b/viz/zone_transitions.py new file mode 100644 index 0000000..8088d48 --- /dev/null +++ b/viz/zone_transitions.py @@ -0,0 +1,105 @@ +# viz/zone_transitions.py +import streamlit as st +import pandas as pd +import os +from datetime import datetime, timedelta, timezone + +def zone_transitions(): + st.title("🔄 Zone Transitions") + + log_file = "reports/zone_transition_log.csv" + if not os.path.exists(log_file): + st.warning("Zone transition log not found.") + return + + try: + # Read the CSV file + df = pd.read_csv(log_file) + + # Standardize column names - convert 'Date' to 'Timestamp' if needed + if 'Date' in df.columns and 'Timestamp' not in df.columns: + df = df.rename(columns={'Date': 'Timestamp'}) + + if 'Timestamp' not in df.columns: + st.error("No valid date column found in the CSV file.") + return + + # Parse the timestamp column with proper format handling + df['Timestamp'] = pd.to_datetime(df['Timestamp'], format='mixed', utc=True) + + # Convert to local timezone for comparison + df['Timestamp'] = df['Timestamp'].dt.tz_convert(None) # Remove timezone info + + # Filter for last 24 hours only + cutoff_time = datetime.now() - timedelta(hours=24) + + # Filter dataframe for last 24 hours + df_recent = df[df['Timestamp'] >= cutoff_time].copy() + + # Sort by timestamp (most recent first) + df_recent = df_recent.sort_values(by='Timestamp', ascending=False) + + # Display results + if df_recent.empty: + st.info("No zone transitions detected in the last 24 hours.") + + # Show some info about what was filtered out + if not df.empty: + total_transitions = len(df) + oldest = df['Timestamp'].min() + newest = df['Timestamp'].max() + filtered_out = total_transitions - len(df_recent) + + st.write(f"📊 **Data Summary:**") + st.write(f"- Total transitions in file: **{total_transitions}**") + st.write(f"- Filtered out (older than 24h): **{filtered_out}**") + st.write(f"- Full data range: {oldest.strftime('%Y-%m-%d %H:%M')} to {newest.strftime('%Y-%m-%d %H:%M')}") + st.write(f"- Cutoff time: {cutoff_time.strftime('%Y-%m-%d %H:%M')}") + + # Debug: show some sample data + with st.expander("🔍 Debug: Recent vs Old Data"): + st.write("**Recent data (should show):**") + recent_debug = df[df['Timestamp'] >= cutoff_time].head(3) + st.write(recent_debug[['Timestamp', 'Ticker']] if not recent_debug.empty else "None") + + st.write("**Old data (filtered out):**") + old_debug = df[df['Timestamp'] < cutoff_time].head(3) + st.write(old_debug[['Timestamp', 'Ticker']] if not old_debug.empty else "None") + else: + st.success(f"Found {len(df_recent)} zone transitions in the last 24 hours") + + # Show time range of displayed data + latest_date = df_recent['Timestamp'].max() + oldest_date = df_recent['Timestamp'].min() + st.info(f"📅 Showing transitions from: {oldest_date.strftime('%Y-%m-%d %H:%M')} to {latest_date.strftime('%Y-%m-%d %H:%M')}") + + # Display the dataframe + st.dataframe(df_recent, use_container_width=True) + + # Add download button for recent data + csv = df_recent.to_csv(index=False).encode("utf-8") + st.download_button( + "đŸ“Ĩ Download Recent Transitions CSV", + csv, + file_name="zone_transitions_24h.csv", + mime="text/csv" + ) + + # Show breakdown by ticker + if len(df_recent) > 0: + st.subheader("📈 Breakdown by Ticker") + ticker_counts = df_recent['Ticker'].value_counts() + st.bar_chart(ticker_counts) + + except Exception as e: + st.error(f"Failed to load zone transitions: {e}") + st.write("Error details:", str(e)) + + # Try to show what's actually in the file for debugging + try: + df_sample = pd.read_csv(log_file, nrows=5) + st.write("First few lines of the CSV file:") + st.write(df_sample) + st.write("Available columns:", list(df_sample.columns)) + except Exception as debug_e: + st.write("Could not read CSV file at all:", str(debug_e)) \ No newline at end of file