Initial Streamlit Cloud deploy

This commit is contained in:
St.jess777
2025-08-29 11:10:28 +02:00
commit 76549424fe
44 changed files with 5063 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.venv/
venv/
ENV/
*.egg-info/
# OS
.DS_Store
Thumbs.db
# Local outputs
reports/
*.xlsx
*.csv
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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<ipython-input-1-6bd7e970ea27>\u001b[0m in \u001b[0;36m<module>\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<ipython-input-1-6bd7e970ea27>\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
}
@@ -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
}
+200
View File
@@ -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
}
@@ -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
}
@@ -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
}
+58
View File
@@ -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))
+5
View File
@@ -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.")
View File
View File
View File
+22
View File
@@ -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'))
+92
View File
@@ -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"
)
View File
+41
View File
@@ -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)
+40
View File
@@ -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')
View File
+35
View File
@@ -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)
+38
View File
@@ -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%}"
}
+67
View File
@@ -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.")
+60
View File
@@ -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
@@ -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
+45
View File
@@ -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
+12
View File
@@ -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
+78
View File
@@ -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()
+137
View File
@@ -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()
View File
+36
View File
@@ -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()
View File
+168
View File
@@ -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)
View File
BIN
View File
Binary file not shown.
+156
View File
@@ -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())
View File
+435
View File
@@ -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='<b>%{y} vs %{x}</b><br>Correlation: %{z:.3f}<br><extra></extra>'
))
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
""")
+192
View File
@@ -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='<b>%{y}/%{x}</b><br>Change: %{z:.2f}%<extra></extra>'
))
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")
+213
View File
@@ -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("""
<style>
.main-header {
font-size: 3rem;
font-weight: 700;
color: #1f2937;
text-align: center;
margin-bottom: 1rem;
background: linear-gradient(90deg, #3b82f6 0%, #1e40af 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.sub-header {
font-size: 1.2rem;
color: #6b7280;
text-align: center;
margin-bottom: 2rem;
}
.feature-card {
background: linear-gradient(145deg, #f1f5f9 0%, #e2e8f0 100%);
padding: 1.5rem;
border-radius: 12px;
border-left: 4px solid #3b82f6;
margin: 1rem 0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
color: #1f2937;
}
.zone-explanation {
background: linear-gradient(145deg, #f0f9ff 0%, #e0f2fe 100%);
padding: 2rem;
border-radius: 12px;
border: 2px solid #0ea5e9;
margin: 2rem 0;
color: #1f2937;
}
.zone-explanation h3 {
color: #1e40af;
margin-bottom: 1rem;
}
.zone-explanation p {
color: #374151;
font-size: 1.1rem;
line-height: 1.6;
}
.zone-category {
background: #f8fafc;
padding: 1rem;
border-radius: 8px;
margin: 0.5rem 0;
border-left: 3px solid;
color: #374151;
}
.zone-reset { border-left-color: #dc2626; }
.zone-budget { border-left-color: #059669; }
.zone-premium { border-left-color: #d97706; }
.metric-card {
background: linear-gradient(145deg, #f8fafc 0%, #e2e8f0 100%);
padding: 1rem;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
color: #374151;
}
</style>
""", unsafe_allow_html=True)
# Header Section
st.markdown('<h1 class="main-header">FX Quant Scan</h1>', unsafe_allow_html=True)
st.markdown('<p class="sub-header">Professional FX Analytics & Zone-Based Market Analysis Platform</p>', unsafe_allow_html=True)
# Quick Stats Row
col1, col2, col3, col4 = st.columns(4)
with col1:
st.markdown('<div class="metric-card"><h3>28</h3><p>Currency Pairs</p></div>', unsafe_allow_html=True)
with col2:
st.markdown('<div class="metric-card"><h3>6</h3><p>Analysis Tools</p></div>', unsafe_allow_html=True)
with col3:
st.markdown('<div class="metric-card"><h3>Real-time</h3><p>Market Data</p></div>', unsafe_allow_html=True)
with col4:
st.markdown('<div class="metric-card"><h3>Zone-Based</h3><p>Price Analysis</p></div>', 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"""
<div class="feature-card">
<h3>{feature['title']}</h3>
<p><strong>{feature['description']}</strong></p>
<p style="color: #059669; font-style: italic;">{feature['use_case']}</p>
</div>
""", unsafe_allow_html=True)
st.markdown("---")
# Zone Analysis Explanation
st.markdown("## 🎯 Understanding Zone Analysis")
st.markdown("""
<div class="zone-explanation">
<h3>💡 What Are Trading Zones?</h3>
<p><strong>Zones are price ranges defined by historical key support and resistance levels.</strong> Think of them as "neighborhoods" where currency pairs like to spend time.</p>
</div>
""", 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("""
<div class="zone-category zone-reset">
<h4>🔴 RESET Zone</h4>
<p><strong>Worst Case Scenario</strong></p>
<p>Currency pair is seeing new historical lows. Potential oversold conditions, but also possible fundamental deterioration.</p>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown("""
<div class="zone-category zone-budget">
<h4>🟢 BUDGET Zone</h4>
<p><strong>Fair Value Range</strong></p>
<p>Currency pair is trading around historical average levels. This represents "normal" or "fair" pricing based on historical patterns.</p>
</div>
""", unsafe_allow_html=True)
with col3:
st.markdown("""
<div class="zone-category zone-premium">
<h4>🟠 PREMIUM+ Zone</h4>
<p><strong>Expensive Territory</strong></p>
<p>Currency pair is at historically high levels. May indicate overbought conditions or strong fundamental drivers.</p>
</div>
""", 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("""
<div style="text-align: center; color: #6b7280; padding: 2rem;">
<p><strong>FX Quant Scan</strong> - Professional FX Analytics Platform</p>
<p>Built for traders who value data-driven decisions and quantitative analysis</p>
</div>
""", unsafe_allow_html=True)
if __name__ == "__main__":
home()
+289
View File
@@ -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='<b>%{y}</b><br>Strength: %{x:.2f}%<br>Rank: #%{customdata}<extra></extra>',
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
""")
+386
View File
@@ -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("""
<style>
.zone-header {
background: linear-gradient(90deg, #1e40af 0%, #3b82f6 100%);
color: white;
padding: 1.5rem;
border-radius: 10px;
margin-bottom: 2rem;
text-align: center;
}
.zone-card {
background: linear-gradient(145deg, #f8fafc 0%, #e2e8f0 100%);
padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
margin: 1rem 0;
border-left: 5px solid;
color: #374151;
}
.zone-reset { border-left-color: #A9A9A9; background: linear-gradient(145deg, #f8fafc 0%, #f1f5f9 100%); }
.zone-clearance { border-left-color: #FF5555; background: linear-gradient(145deg, #fef2f2 0%, #fecaca 100%); }
.zone-discount { border-left-color: #FF9999; background: linear-gradient(145deg, #fff5f5 0%, #fed7d7 100%); }
.zone-budget { border-left-color: #ADD8E6; background: linear-gradient(145deg, #eff6ff 0%, #dbeafe 100%); }
.zone-fair { border-left-color: #90EE90; background: linear-gradient(145deg, #f0fdf4 0%, #dcfce7 100%); }
.zone-plus { border-left-color: #FFA500; background: linear-gradient(145deg, #fffbeb 0%, #fed7aa 100%); }
.zone-premium { border-left-color: #FFD700; background: linear-gradient(145deg, #fefce8 0%, #fef08a 100%); }
.zone-premium-plus { border-left-color: #DA70D6; background: linear-gradient(145deg, #faf5ff 0%, #e9d5ff 100%); }
.metric-box {
background: linear-gradient(145deg, #f8fafc 0%, #e2e8f0 100%);
padding: 1rem;
border-radius: 8px;
text-align: center;
margin: 0.5rem 0;
border: 2px solid #e2e8f0;
}
.zone-legend {
background: linear-gradient(145deg, #f0f9ff 0%, #e0f2fe 100%);
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
color: #374151;
}
</style>
""", unsafe_allow_html=True)
# Header
st.markdown("""
<div class="zone-header">
<h1>🎯 Zone Locator</h1>
<p>Identify if currency pairs are cheap, fairly priced, or expensive based on historical zones</p>
</div>
""", 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"""
<div class="zone-legend">
<div style="background: {info['color']}; height: 4px; margin-bottom: 8px; border-radius: 2px;"></div>
<strong>{zone}</strong><br>
<small>{info['desc']}</small>
</div>
""", 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"""
<div class="zone-card {zone_class}">
<div style="font-size: 1.2rem; font-weight: 600; margin-bottom: 0.5rem;">Current Zone</div>
<div style="font-size: 2rem; font-weight: bold; color: {zone_color};">
{current_zone}
</div>
<div style="font-size: 1.5rem; font-weight: bold; color: #1f2937;">{current_price:.5f}</div>
<div style="color: #6b7280; font-size: 0.9rem;">
Updated: {datetime.now().strftime('%H:%M:%S')}
</div>
</div>
""", 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"""
<div class="zone-card {zone_class}">
<h3>{interpretation['emoji']} {interpretation['title']}</h3>
<p><strong>{interpretation['description']}</strong></p>
<p><strong>Strategy Considerations:</strong> {interpretation['strategy']}</p>
<p><strong>Risk Level:</strong> {interpretation['risk']}</p>
</div>
""", 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("""
<div style="text-align: center; color: #6b7280; padding: 1rem;">
<p><strong>Zone Locator</strong> - Historical zone analysis for informed trading decisions</p>
</div>
""", unsafe_allow_html=True)
if __name__ == "__main__":
zone_locator()
+105
View File
@@ -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))