Update app.py
Browse files
app.py
CHANGED
|
@@ -43,7 +43,55 @@ symbol_volumes = {
|
|
| 43 |
"EURUSD": 0.01,
|
| 44 |
"_DEFAULT_FOREX": 0.01, # catch-all for any forex pair not explicitly listed
|
| 45 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
logger.info(f"Per-symbol volumes initialized: {symbol_volumes}")
|
|
|
|
| 47 |
|
| 48 |
# Global caches to eliminate latency of symbol resolution and limits querying
|
| 49 |
SYMBOL_ID_CACHE = {} # symbol_name_upper -> symbol_id
|
|
@@ -782,20 +830,44 @@ async def dashboard(request: Request):
|
|
| 782 |
if sym not in symbol_volumes:
|
| 783 |
continue
|
| 784 |
vol = symbol_volumes[sym]
|
|
|
|
|
|
|
|
|
|
| 785 |
display_name = "Default Forex" if sym == "_DEFAULT_FOREX" else sym
|
| 786 |
row_bg = "#0f172a" if display_order.index(sym) % 2 == 0 else "#111827"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
volume_rows_html += f'''
|
| 788 |
<tr style="border-bottom: 1px solid #1e293b; background: {row_bg};">
|
| 789 |
<td style="padding: 8px 12px; color: #e2e8f0; font-size: 14px; font-weight: 600;">{display_name}</td>
|
| 790 |
<td style="padding: 8px 12px;">
|
| 791 |
-
<form action="/set-volume" method="post" style="display: flex; gap: 8px; align-items: center;">
|
| 792 |
<input type="hidden" name="symbol" value="{sym}">
|
| 793 |
<input type="text" name="volume" value="{vol:.2f}"
|
| 794 |
style="background: #1e293b; border: 1px solid #334155; border-radius: 6px; padding: 6px 10px; color: #e2e8f0; font-size: 14px; width: 80px;">
|
| 795 |
<button type="submit" class="btn" style="padding: 6px 14px; font-size: 12px;">Update</button>
|
| 796 |
</form>
|
| 797 |
</td>
|
| 798 |
-
<td style="padding: 8px 12px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 799 |
</tr>'''
|
| 800 |
|
| 801 |
# 2. Construct OAuth status card HTML
|
|
@@ -834,7 +906,7 @@ async def dashboard(request: Request):
|
|
| 834 |
<tr style="border-bottom: 1px solid #334155;">
|
| 835 |
<th style="text-align: left; padding: 8px 12px; color: #94a3b8; font-size: 13px;">Symbol</th>
|
| 836 |
<th style="text-align: left; padding: 8px 12px; color: #94a3b8; font-size: 13px;">Lot Size</th>
|
| 837 |
-
<th style="padding: 8px 12px;"></th>
|
| 838 |
</tr>
|
| 839 |
</thead>
|
| 840 |
<tbody>
|
|
@@ -1151,6 +1223,7 @@ async def set_volume(request: Request, symbol: str = Form(...), volume: str = Fo
|
|
| 1151 |
vol_float = float(volume.strip())
|
| 1152 |
symbol_upper = symbol.strip().upper()
|
| 1153 |
symbol_volumes[symbol_upper] = vol_float
|
|
|
|
| 1154 |
logger.info(f"Trade volume for symbol '{symbol_upper}' updated to {vol_float} lots.")
|
| 1155 |
except Exception as e:
|
| 1156 |
logger.error(f"Invalid volume update for symbol '{symbol}': {volume}. Error: {e}")
|
|
@@ -1164,6 +1237,31 @@ async def set_volume(request: Request, symbol: str = Form(...), volume: str = Fo
|
|
| 1164 |
redirect_url = f"{protocol}://{host}/"
|
| 1165 |
return RedirectResponse(redirect_url, status_code=303)
|
| 1166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1167 |
@app.post("/webhook")
|
| 1168 |
async def webhook(request: Request):
|
| 1169 |
# Parse Webhook JSON data
|
|
@@ -1178,6 +1276,19 @@ async def webhook(request: Request):
|
|
| 1178 |
|
| 1179 |
if action not in ["BUY", "SELL", "STOP-TRADE", "CLOSE", "ANCHOR"]:
|
| 1180 |
raise HTTPException(status_code=400, detail="Invalid action. Must be BUY, SELL, STOP-TRADE, CLOSE, or ANCHOR")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1181 |
|
| 1182 |
# Load and refresh tokens
|
| 1183 |
tokens = get_valid_tokens()
|
|
|
|
| 43 |
"EURUSD": 0.01,
|
| 44 |
"_DEFAULT_FOREX": 0.01, # catch-all for any forex pair not explicitly listed
|
| 45 |
}
|
| 46 |
+
|
| 47 |
+
# Per-symbol BUY/SELL toggle (controlled from dashboard, NOT TradingView)
|
| 48 |
+
symbol_status = {
|
| 49 |
+
"NAS100": {"buy": True, "sell": True},
|
| 50 |
+
"NAS100m": {"buy": True, "sell": True},
|
| 51 |
+
"USTEC": {"buy": True, "sell": True},
|
| 52 |
+
"US500": {"buy": True, "sell": True},
|
| 53 |
+
"US500m": {"buy": True, "sell": True},
|
| 54 |
+
"US30": {"buy": True, "sell": True},
|
| 55 |
+
"US30m": {"buy": True, "sell": True},
|
| 56 |
+
"BTCUSD": {"buy": True, "sell": True},
|
| 57 |
+
"EURUSD": {"buy": True, "sell": True},
|
| 58 |
+
"_DEFAULT_FOREX": {"buy": True, "sell": True},
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
CONFIG_FILE = "bot_config.json"
|
| 62 |
+
|
| 63 |
+
def load_config():
|
| 64 |
+
"""Load persisted volumes + status toggles from disk."""
|
| 65 |
+
global symbol_volumes, symbol_status
|
| 66 |
+
if os.path.exists(CONFIG_FILE):
|
| 67 |
+
try:
|
| 68 |
+
with open(CONFIG_FILE, "r") as f_conf:
|
| 69 |
+
config = json.load(f_conf)
|
| 70 |
+
if "volumes" in config:
|
| 71 |
+
symbol_volumes.update(config["volumes"])
|
| 72 |
+
if "status" in config:
|
| 73 |
+
for k, v in config["status"].items():
|
| 74 |
+
if k in symbol_status:
|
| 75 |
+
symbol_status[k].update(v)
|
| 76 |
+
else:
|
| 77 |
+
symbol_status[k] = v
|
| 78 |
+
logger.info("Configuration loaded from bot_config.json")
|
| 79 |
+
except Exception as e:
|
| 80 |
+
logger.error(f"Error loading bot_config.json: {e}")
|
| 81 |
+
|
| 82 |
+
def save_config():
|
| 83 |
+
"""Persist current volumes + status toggles to disk."""
|
| 84 |
+
try:
|
| 85 |
+
config = {"volumes": symbol_volumes, "status": symbol_status}
|
| 86 |
+
with open(CONFIG_FILE, "w") as f_conf:
|
| 87 |
+
json.dump(config, f_conf, indent=4)
|
| 88 |
+
logger.info("Configuration saved to bot_config.json")
|
| 89 |
+
except Exception as e:
|
| 90 |
+
logger.error(f"Error saving bot_config.json: {e}")
|
| 91 |
+
|
| 92 |
+
load_config()
|
| 93 |
logger.info(f"Per-symbol volumes initialized: {symbol_volumes}")
|
| 94 |
+
logger.info(f"Per-symbol statuses initialized: {symbol_status}")
|
| 95 |
|
| 96 |
# Global caches to eliminate latency of symbol resolution and limits querying
|
| 97 |
SYMBOL_ID_CACHE = {} # symbol_name_upper -> symbol_id
|
|
|
|
| 830 |
if sym not in symbol_volumes:
|
| 831 |
continue
|
| 832 |
vol = symbol_volumes[sym]
|
| 833 |
+
status = symbol_status.get(sym, {"buy": True, "sell": True})
|
| 834 |
+
buy_on = status.get("buy", True)
|
| 835 |
+
sell_on = status.get("sell", True)
|
| 836 |
display_name = "Default Forex" if sym == "_DEFAULT_FOREX" else sym
|
| 837 |
row_bg = "#0f172a" if display_order.index(sym) % 2 == 0 else "#111827"
|
| 838 |
+
# BUY toggle button: green if ON, grey if OFF
|
| 839 |
+
buy_color = "#00E676" if buy_on else "#475569"
|
| 840 |
+
buy_text = "BUY β" if buy_on else "BUY β"
|
| 841 |
+
buy_val = "false" if buy_on else "true" # clicking toggles to opposite
|
| 842 |
+
# SELL toggle button: red if ON, grey if OFF
|
| 843 |
+
sell_color = "#FF5252" if sell_on else "#475569"
|
| 844 |
+
sell_text = "SELL β" if sell_on else "SELL β"
|
| 845 |
+
sell_val = "false" if sell_on else "true"
|
| 846 |
volume_rows_html += f'''
|
| 847 |
<tr style="border-bottom: 1px solid #1e293b; background: {row_bg};">
|
| 848 |
<td style="padding: 8px 12px; color: #e2e8f0; font-size: 14px; font-weight: 600;">{display_name}</td>
|
| 849 |
<td style="padding: 8px 12px;">
|
| 850 |
+
<form action="/set-volume" method="post" style="display: inline-flex; gap: 8px; align-items: center;">
|
| 851 |
<input type="hidden" name="symbol" value="{sym}">
|
| 852 |
<input type="text" name="volume" value="{vol:.2f}"
|
| 853 |
style="background: #1e293b; border: 1px solid #334155; border-radius: 6px; padding: 6px 10px; color: #e2e8f0; font-size: 14px; width: 80px;">
|
| 854 |
<button type="submit" class="btn" style="padding: 6px 14px; font-size: 12px;">Update</button>
|
| 855 |
</form>
|
| 856 |
</td>
|
| 857 |
+
<td style="padding: 8px 12px;">
|
| 858 |
+
<form action="/toggle-status" method="post" style="display: inline-flex; gap: 6px;">
|
| 859 |
+
<input type="hidden" name="symbol" value="{sym}">
|
| 860 |
+
<input type="hidden" name="direction" value="buy">
|
| 861 |
+
<input type="hidden" name="new_value" value="{buy_val}">
|
| 862 |
+
<button type="submit" style="background: {buy_color}; color: white; border: none; border-radius: 6px; padding: 6px 12px; font-size: 12px; font-weight: 700; cursor: pointer;">{buy_text}</button>
|
| 863 |
+
</form>
|
| 864 |
+
<form action="/toggle-status" method="post" style="display: inline-flex; gap: 6px;">
|
| 865 |
+
<input type="hidden" name="symbol" value="{sym}">
|
| 866 |
+
<input type="hidden" name="direction" value="sell">
|
| 867 |
+
<input type="hidden" name="new_value" value="{sell_val}">
|
| 868 |
+
<button type="submit" style="background: {sell_color}; color: white; border: none; border-radius: 6px; padding: 6px 12px; font-size: 12px; font-weight: 700; cursor: pointer;">{sell_text}</button>
|
| 869 |
+
</form>
|
| 870 |
+
</td>
|
| 871 |
</tr>'''
|
| 872 |
|
| 873 |
# 2. Construct OAuth status card HTML
|
|
|
|
| 906 |
<tr style="border-bottom: 1px solid #334155;">
|
| 907 |
<th style="text-align: left; padding: 8px 12px; color: #94a3b8; font-size: 13px;">Symbol</th>
|
| 908 |
<th style="text-align: left; padding: 8px 12px; color: #94a3b8; font-size: 13px;">Lot Size</th>
|
| 909 |
+
<th style="text-align: left; padding: 8px 12px; color: #94a3b8; font-size: 13px;">Allow Trade</th>
|
| 910 |
</tr>
|
| 911 |
</thead>
|
| 912 |
<tbody>
|
|
|
|
| 1223 |
vol_float = float(volume.strip())
|
| 1224 |
symbol_upper = symbol.strip().upper()
|
| 1225 |
symbol_volumes[symbol_upper] = vol_float
|
| 1226 |
+
save_config()
|
| 1227 |
logger.info(f"Trade volume for symbol '{symbol_upper}' updated to {vol_float} lots.")
|
| 1228 |
except Exception as e:
|
| 1229 |
logger.error(f"Invalid volume update for symbol '{symbol}': {volume}. Error: {e}")
|
|
|
|
| 1237 |
redirect_url = f"{protocol}://{host}/"
|
| 1238 |
return RedirectResponse(redirect_url, status_code=303)
|
| 1239 |
|
| 1240 |
+
@app.post("/toggle-status")
|
| 1241 |
+
async def toggle_status(request: Request, symbol: str = Form(...), direction: str = Form(...), new_value: str = Form(...)):
|
| 1242 |
+
"""Instantly toggle Allow BUY or Allow SELL for a symbol from the dashboard."""
|
| 1243 |
+
global symbol_status
|
| 1244 |
+
symbol_upper = symbol.strip().upper()
|
| 1245 |
+
dir_lower = direction.strip().lower()
|
| 1246 |
+
new_bool = new_value.strip().lower() == "true"
|
| 1247 |
+
|
| 1248 |
+
if symbol_upper not in symbol_status:
|
| 1249 |
+
symbol_status[symbol_upper] = {"buy": True, "sell": True}
|
| 1250 |
+
symbol_status[symbol_upper][dir_lower] = new_bool
|
| 1251 |
+
save_config()
|
| 1252 |
+
|
| 1253 |
+
state_str = "ENABLED" if new_bool else "DISABLED"
|
| 1254 |
+
logger.info(f"{dir_lower.upper()} {state_str} for {symbol_upper}")
|
| 1255 |
+
|
| 1256 |
+
space_host = os.environ.get("SPACE_HOST")
|
| 1257 |
+
if space_host:
|
| 1258 |
+
redirect_url = f"https://{space_host}/"
|
| 1259 |
+
else:
|
| 1260 |
+
host = request.headers.get("host", "")
|
| 1261 |
+
protocol = "https" if "hf.space" in host or request.headers.get("x-forwarded-proto") == "https" else "http"
|
| 1262 |
+
redirect_url = f"{protocol}://{host}/"
|
| 1263 |
+
return RedirectResponse(redirect_url, status_code=303)
|
| 1264 |
+
|
| 1265 |
@app.post("/webhook")
|
| 1266 |
async def webhook(request: Request):
|
| 1267 |
# Parse Webhook JSON data
|
|
|
|
| 1276 |
|
| 1277 |
if action not in ["BUY", "SELL", "STOP-TRADE", "CLOSE", "ANCHOR"]:
|
| 1278 |
raise HTTPException(status_code=400, detail="Invalid action. Must be BUY, SELL, STOP-TRADE, CLOSE, or ANCHOR")
|
| 1279 |
+
|
| 1280 |
+
# ββ Dashboard BUY/SELL toggle filter ββ
|
| 1281 |
+
# If the user disabled BUY or SELL for this symbol on the dashboard, ignore it instantly
|
| 1282 |
+
if action in ["BUY", "SELL"]:
|
| 1283 |
+
status = symbol_status.get(ticker, symbol_status.get("_DEFAULT_FOREX", {"buy": True, "sell": True}))
|
| 1284 |
+
is_allowed = status.get(action.lower(), True)
|
| 1285 |
+
if not is_allowed:
|
| 1286 |
+
ignore_msg = f"{action} for {ticker} IGNORED β disabled on dashboard"
|
| 1287 |
+
logger.info(ignore_msg)
|
| 1288 |
+
WEBHOOK_LOGS.append({"time": datetime.now().strftime('%H:%M:%S'), "action": f"{action} BLOCKED", "color": "#94a3b8", "msg": ignore_msg})
|
| 1289 |
+
if len(WEBHOOK_LOGS) > 50:
|
| 1290 |
+
WEBHOOK_LOGS.pop(0)
|
| 1291 |
+
return {"status": "ignored", "message": ignore_msg}
|
| 1292 |
|
| 1293 |
# Load and refresh tokens
|
| 1294 |
tokens = get_valid_tokens()
|