mirror of
https://github.com/777r1NTR/FX-QUANT-SCAN.git
synced 2026-07-27 17:37:46 +00:00
Initial Streamlit Cloud deploy
This commit is contained in:
+18
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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.")
|
||||
@@ -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'))
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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%}"
|
||||
}
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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())
|
||||
@@ -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
|
||||
""")
|
||||
@@ -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
@@ -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()
|
||||
@@ -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
|
||||
""")
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user