Update app.py
Browse files
app.py
CHANGED
|
@@ -5,6 +5,7 @@ import ssl
|
|
| 5 |
import struct
|
| 6 |
import logging
|
| 7 |
import time
|
|
|
|
| 8 |
from datetime import datetime
|
| 9 |
import requests
|
| 10 |
from fastapi import FastAPI, Request, Form, HTTPException
|
|
@@ -65,28 +66,53 @@ def load_tokens():
|
|
| 65 |
except Exception as e:
|
| 66 |
logger.error(f"Failed to load tokens: {e}")
|
| 67 |
|
| 68 |
-
# Fallback to
|
| 69 |
env_refresh = os.environ.get("CTRADER_REFRESH_TOKEN")
|
|
|
|
| 70 |
env_acc_id = os.environ.get("CTRADER_ACCOUNT_ID")
|
| 71 |
-
if env_refresh:
|
| 72 |
-
logger.info("Using
|
| 73 |
return {
|
| 74 |
-
"refresh_token": env_refresh.strip(),
|
| 75 |
-
"access_token": "",
|
| 76 |
-
"account_id": env_acc_id.strip() if env_acc_id else ""
|
|
|
|
| 77 |
}
|
| 78 |
return None
|
| 79 |
|
| 80 |
# Helper: Exchange authorization code or refresh token
|
| 81 |
def fetch_token_from_api(payload):
|
| 82 |
url = "https://openapi.ctrader.com/apps/token"
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
return None
|
| 91 |
|
| 92 |
# Helper: Send protocol buffer message over SSL Socket
|
|
@@ -230,6 +256,71 @@ def resolve_symbol_id(ssl_sock, account_id, symbol_name):
|
|
| 230 |
break
|
| 231 |
return None
|
| 232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
# Helper: Fetch Account Balance/Details
|
| 234 |
def get_trader_details(ssl_sock, account_id):
|
| 235 |
req = ProtoOATraderReq()
|
|
@@ -368,8 +459,13 @@ def place_market_order(ssl_sock, account_id, symbol_id, trade_side, volume, posi
|
|
| 368 |
logger.info(f"place_market_order: Received other payloadType: {proto_msg.payloadType}")
|
| 369 |
return {"status": "error", "message": "No execution response received"}
|
| 370 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
# Helper: Refresh tokens and return active ones
|
| 372 |
def get_valid_tokens():
|
|
|
|
| 373 |
tokens = load_tokens()
|
| 374 |
if not tokens:
|
| 375 |
return None
|
|
@@ -379,7 +475,14 @@ def get_valid_tokens():
|
|
| 379 |
updated_at = tokens.get("updated_at", 0)
|
| 380 |
|
| 381 |
# Only refresh if the access token is missing or older than 24 hours (86400 seconds)
|
|
|
|
| 382 |
if not access_token or (time.time() - updated_at > 86400):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
if refresh_token:
|
| 384 |
payload = {
|
| 385 |
"grant_type": "refresh_token",
|
|
@@ -460,8 +563,10 @@ def get_valid_tokens():
|
|
| 460 |
|
| 461 |
@app.get("/", response_class=HTMLResponse)
|
| 462 |
async def dashboard(request: Request):
|
| 463 |
-
|
| 464 |
-
|
|
|
|
|
|
|
| 465 |
account_id = tokens.get("account_id", "") if is_authenticated else ""
|
| 466 |
access_token = tokens.get("access_token", "") if is_authenticated else ""
|
| 467 |
|
|
@@ -872,8 +977,8 @@ async def webhook(request: Request):
|
|
| 872 |
ticker = data.get("ticker", "").upper()
|
| 873 |
price = data.get("price")
|
| 874 |
|
| 875 |
-
if action not in ["BUY", "SELL", "STOP-TRADE", "CLOSE"]:
|
| 876 |
-
raise HTTPException(status_code=400, detail="Invalid action. Must be BUY, SELL, STOP-TRADE, or
|
| 877 |
|
| 878 |
# Load and refresh tokens
|
| 879 |
tokens = get_valid_tokens()
|
|
@@ -884,7 +989,7 @@ async def webhook(request: Request):
|
|
| 884 |
access_token = tokens["access_token"]
|
| 885 |
|
| 886 |
log_time = datetime.now().strftime("%H:%M:%S")
|
| 887 |
-
color = "#60a5fa" if action == "BUY" else "#f87171" if action == "SELL" else "#fb923c" if action == "CLOSE" else "#94a3b8"
|
| 888 |
|
| 889 |
try:
|
| 890 |
# Establish SSL Socket connection to cTrader Open API
|
|
@@ -900,6 +1005,16 @@ async def webhook(request: Request):
|
|
| 900 |
positions = get_open_positions(ssl_sock, account_id)
|
| 901 |
symbol_positions = [p for p in positions if p.tradeData.symbolId == symbol_id]
|
| 902 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 903 |
execution_msg = ""
|
| 904 |
|
| 905 |
if action == "BUY":
|
|
@@ -913,7 +1028,7 @@ async def webhook(request: Request):
|
|
| 913 |
else:
|
| 914 |
# Open new BUY position (existing positions stay untouched — only stop-trade/end-trade closes them)
|
| 915 |
logger.info(f"Opening new Long position... ({losing_count} losing BUY(s) open, limit {MAX_LOSING_POSITIONS})")
|
| 916 |
-
res =
|
| 917 |
execution_msg = f"Opened BUY position for {ticker}. Order Result: {res.get('status')} {res.get('message', '')}"
|
| 918 |
|
| 919 |
elif action == "SELL":
|
|
@@ -927,7 +1042,7 @@ async def webhook(request: Request):
|
|
| 927 |
else:
|
| 928 |
# Open new SELL position (existing positions stay untouched — only stop-trade/end-trade closes them)
|
| 929 |
logger.info(f"Opening new Short position... ({losing_count} losing SELL(s) open, limit {MAX_LOSING_POSITIONS})")
|
| 930 |
-
res =
|
| 931 |
execution_msg = f"Opened SELL position for {ticker}. Order Result: {res.get('status')} {res.get('message', '')}"
|
| 932 |
|
| 933 |
elif action == "CLOSE":
|
|
@@ -979,6 +1094,42 @@ async def webhook(request: Request):
|
|
| 979 |
else:
|
| 980 |
execution_msg = f"No open positions on {ticker} to close."
|
| 981 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 982 |
ssl_sock.close()
|
| 983 |
|
| 984 |
# Log to in-memory dashboard logs
|
|
|
|
| 5 |
import struct
|
| 6 |
import logging
|
| 7 |
import time
|
| 8 |
+
import asyncio
|
| 9 |
from datetime import datetime
|
| 10 |
import requests
|
| 11 |
from fastapi import FastAPI, Request, Form, HTTPException
|
|
|
|
| 66 |
except Exception as e:
|
| 67 |
logger.error(f"Failed to load tokens: {e}")
|
| 68 |
|
| 69 |
+
# Fallback to environment secrets if token file doesn't exist
|
| 70 |
env_refresh = os.environ.get("CTRADER_REFRESH_TOKEN")
|
| 71 |
+
env_access = os.environ.get("CTRADER_ACCESS_TOKEN")
|
| 72 |
env_acc_id = os.environ.get("CTRADER_ACCOUNT_ID")
|
| 73 |
+
if env_refresh or env_access:
|
| 74 |
+
logger.info("Using tokens from environment secrets")
|
| 75 |
return {
|
| 76 |
+
"refresh_token": env_refresh.strip() if env_refresh else "",
|
| 77 |
+
"access_token": env_access.strip() if env_access else "",
|
| 78 |
+
"account_id": env_acc_id.strip() if env_acc_id else "",
|
| 79 |
+
"updated_at": time.time()
|
| 80 |
}
|
| 81 |
return None
|
| 82 |
|
| 83 |
# Helper: Exchange authorization code or refresh token
|
| 84 |
def fetch_token_from_api(payload):
|
| 85 |
url = "https://openapi.ctrader.com/apps/token"
|
| 86 |
+
headers = {
|
| 87 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
| 88 |
+
"Accept": "application/json, text/plain, */*"
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
# Attempt 1: GET request with custom headers
|
| 92 |
+
try:
|
| 93 |
+
res = requests.get(url, params=payload, headers=headers, timeout=10)
|
| 94 |
+
if res.status_code == 200:
|
| 95 |
+
data = res.json()
|
| 96 |
+
logger.info("Token request successful (GET)")
|
| 97 |
+
return data
|
| 98 |
+
except Exception as e:
|
| 99 |
+
logger.warning(f"GET token request exception: {e}")
|
| 100 |
+
|
| 101 |
+
# Attempt 2: POST request fallback with form-encoded data
|
| 102 |
+
try:
|
| 103 |
+
res = requests.post(url, data=payload, headers=headers, timeout=10)
|
| 104 |
+
if res.status_code == 200:
|
| 105 |
+
data = res.json()
|
| 106 |
+
logger.info("Token request successful (POST)")
|
| 107 |
+
return data
|
| 108 |
+
elif res.status_code == 429:
|
| 109 |
+
logger.warning("cTrader API rate limited (HTTP 429). Cloudflare/WAF blocked request.")
|
| 110 |
+
return None
|
| 111 |
+
else:
|
| 112 |
+
logger.error(f"Token exchange failed (HTTP {res.status_code}): {res.text[:200]}")
|
| 113 |
+
return None
|
| 114 |
+
except Exception as e:
|
| 115 |
+
logger.error(f"POST token request exception: {e}")
|
| 116 |
return None
|
| 117 |
|
| 118 |
# Helper: Send protocol buffer message over SSL Socket
|
|
|
|
| 256 |
break
|
| 257 |
return None
|
| 258 |
|
| 259 |
+
# Helper: Get volume limits for a symbol from broker
|
| 260 |
+
def get_symbol_volume_limits(ssl_sock, account_id, symbol_id):
|
| 261 |
+
"""Query the broker for the symbol's volume limits.
|
| 262 |
+
Returns dict with maxVolume, minVolume, stepVolume in protocol units (units * 100), or empty dict."""
|
| 263 |
+
result = {}
|
| 264 |
+
try:
|
| 265 |
+
req = ProtoOASymbolByIdReq()
|
| 266 |
+
req.ctidTraderAccountId = int(account_id)
|
| 267 |
+
req.symbolId.append(int(symbol_id))
|
| 268 |
+
send_proto_message(ssl_sock, req, 2116)
|
| 269 |
+
|
| 270 |
+
while True:
|
| 271 |
+
proto_msg = receive_proto_message(ssl_sock)
|
| 272 |
+
if not proto_msg:
|
| 273 |
+
break
|
| 274 |
+
if proto_msg.payloadType == 2117: # ProtoOASymbolByIdRes
|
| 275 |
+
res = ProtoOASymbolByIdRes()
|
| 276 |
+
res.ParseFromString(proto_msg.payload)
|
| 277 |
+
for sym in res.symbol:
|
| 278 |
+
if hasattr(sym, 'maxVolume') and sym.maxVolume:
|
| 279 |
+
result['maxVolume'] = sym.maxVolume
|
| 280 |
+
if hasattr(sym, 'minVolume') and sym.minVolume:
|
| 281 |
+
result['minVolume'] = sym.minVolume
|
| 282 |
+
if hasattr(sym, 'stepVolume') and sym.stepVolume:
|
| 283 |
+
result['stepVolume'] = sym.stepVolume
|
| 284 |
+
logger.info(f"Symbol {symbol_id} volume limits: max={result.get('maxVolume', 'N/A')}, min={result.get('minVolume', 'N/A')}, step={result.get('stepVolume', 'N/A')} protocol units")
|
| 285 |
+
break
|
| 286 |
+
except Exception as e:
|
| 287 |
+
logger.warning(f"Could not fetch volume limits for symbol {symbol_id}: {e}")
|
| 288 |
+
return result
|
| 289 |
+
|
| 290 |
+
# Helper: Place market order with automatic volume reduction on "Not enough funds"
|
| 291 |
+
def place_market_order_smart(ssl_sock, account_id, symbol_id, trade_side, volume, min_volume=100, step_volume=100):
|
| 292 |
+
"""Try placing an order. If 'Not enough funds', halve volume and retry until min_volume."""
|
| 293 |
+
attempt = 0
|
| 294 |
+
try_volume = volume
|
| 295 |
+
|
| 296 |
+
while try_volume >= min_volume:
|
| 297 |
+
attempt += 1
|
| 298 |
+
# Round down to nearest step
|
| 299 |
+
if step_volume > 0:
|
| 300 |
+
try_volume = (try_volume // step_volume) * step_volume
|
| 301 |
+
if try_volume < min_volume:
|
| 302 |
+
break
|
| 303 |
+
|
| 304 |
+
logger.info(f"Order attempt {attempt}: volume={try_volume} protocol units ({try_volume / 100:.2f} units)")
|
| 305 |
+
res = place_market_order(ssl_sock, account_id, symbol_id, trade_side, int(try_volume))
|
| 306 |
+
|
| 307 |
+
if res.get('status') == 'success':
|
| 308 |
+
if attempt > 1:
|
| 309 |
+
logger.info(f"Order succeeded on attempt {attempt} with reduced volume {try_volume}")
|
| 310 |
+
return res
|
| 311 |
+
|
| 312 |
+
error_msg = res.get('message', '')
|
| 313 |
+
if 'not enough funds' in error_msg.lower() or 'Not enough' in error_msg:
|
| 314 |
+
# Halve the volume and retry
|
| 315 |
+
try_volume = try_volume // 2
|
| 316 |
+
logger.info(f"Not enough funds — reducing volume to {try_volume} and retrying...")
|
| 317 |
+
else:
|
| 318 |
+
# Different error, don't retry
|
| 319 |
+
return res
|
| 320 |
+
|
| 321 |
+
return {"status": "error", "message": f"Not enough funds even at minimum volume ({min_volume} protocol units = {min_volume/100:.2f} units)"}
|
| 322 |
+
|
| 323 |
+
|
| 324 |
# Helper: Fetch Account Balance/Details
|
| 325 |
def get_trader_details(ssl_sock, account_id):
|
| 326 |
req = ProtoOATraderReq()
|
|
|
|
| 459 |
logger.info(f"place_market_order: Received other payloadType: {proto_msg.payloadType}")
|
| 460 |
return {"status": "error", "message": "No execution response received"}
|
| 461 |
|
| 462 |
+
# Rate-limit guard: track last refresh attempt to avoid hammering the API
|
| 463 |
+
_last_refresh_attempt = 0
|
| 464 |
+
REFRESH_COOLDOWN = 60 # seconds between refresh attempts
|
| 465 |
+
|
| 466 |
# Helper: Refresh tokens and return active ones
|
| 467 |
def get_valid_tokens():
|
| 468 |
+
global _last_refresh_attempt
|
| 469 |
tokens = load_tokens()
|
| 470 |
if not tokens:
|
| 471 |
return None
|
|
|
|
| 475 |
updated_at = tokens.get("updated_at", 0)
|
| 476 |
|
| 477 |
# Only refresh if the access token is missing or older than 24 hours (86400 seconds)
|
| 478 |
+
# AND we haven't tried refreshing in the last REFRESH_COOLDOWN seconds
|
| 479 |
if not access_token or (time.time() - updated_at > 86400):
|
| 480 |
+
if time.time() - _last_refresh_attempt < REFRESH_COOLDOWN:
|
| 481 |
+
logger.info(f"Skipping token refresh (cooldown: {REFRESH_COOLDOWN - (time.time() - _last_refresh_attempt):.0f}s remaining)")
|
| 482 |
+
if access_token:
|
| 483 |
+
return tokens
|
| 484 |
+
return None
|
| 485 |
+
_last_refresh_attempt = time.time()
|
| 486 |
if refresh_token:
|
| 487 |
payload = {
|
| 488 |
"grant_type": "refresh_token",
|
|
|
|
| 563 |
|
| 564 |
@app.get("/", response_class=HTMLResponse)
|
| 565 |
async def dashboard(request: Request):
|
| 566 |
+
# Dashboard uses load_tokens() directly — NO refresh call.
|
| 567 |
+
# This prevents health checks from hammering the cTrader API.
|
| 568 |
+
tokens = load_tokens()
|
| 569 |
+
is_authenticated = tokens is not None and bool(tokens.get("access_token"))
|
| 570 |
account_id = tokens.get("account_id", "") if is_authenticated else ""
|
| 571 |
access_token = tokens.get("access_token", "") if is_authenticated else ""
|
| 572 |
|
|
|
|
| 977 |
ticker = data.get("ticker", "").upper()
|
| 978 |
price = data.get("price")
|
| 979 |
|
| 980 |
+
if action not in ["BUY", "SELL", "STOP-TRADE", "CLOSE", "ANCHOR"]:
|
| 981 |
+
raise HTTPException(status_code=400, detail="Invalid action. Must be BUY, SELL, STOP-TRADE, CLOSE, or ANCHOR")
|
| 982 |
|
| 983 |
# Load and refresh tokens
|
| 984 |
tokens = get_valid_tokens()
|
|
|
|
| 989 |
access_token = tokens["access_token"]
|
| 990 |
|
| 991 |
log_time = datetime.now().strftime("%H:%M:%S")
|
| 992 |
+
color = "#60a5fa" if action == "BUY" else "#f87171" if action == "SELL" else "#fb923c" if action == "CLOSE" else "#FF9800" if action == "ANCHOR" else "#94a3b8"
|
| 993 |
|
| 994 |
try:
|
| 995 |
# Establish SSL Socket connection to cTrader Open API
|
|
|
|
| 1005 |
positions = get_open_positions(ssl_sock, account_id)
|
| 1006 |
symbol_positions = [p for p in positions if p.tradeData.symbolId == symbol_id]
|
| 1007 |
|
| 1008 |
+
# Auto-clamp volume to broker's max for this symbol and get min/step
|
| 1009 |
+
effective_volume = current_trade_volume
|
| 1010 |
+
vol_limits = get_symbol_volume_limits(ssl_sock, account_id, symbol_id)
|
| 1011 |
+
max_vol = vol_limits.get('maxVolume')
|
| 1012 |
+
min_vol = vol_limits.get('minVolume', 100) # default 1 unit
|
| 1013 |
+
step_vol = vol_limits.get('stepVolume', 100) # default 1 unit
|
| 1014 |
+
if max_vol and effective_volume > max_vol:
|
| 1015 |
+
logger.info(f"Clamping volume from {effective_volume} to {max_vol} (broker max for {ticker})")
|
| 1016 |
+
effective_volume = max_vol
|
| 1017 |
+
|
| 1018 |
execution_msg = ""
|
| 1019 |
|
| 1020 |
if action == "BUY":
|
|
|
|
| 1028 |
else:
|
| 1029 |
# Open new BUY position (existing positions stay untouched — only stop-trade/end-trade closes them)
|
| 1030 |
logger.info(f"Opening new Long position... ({losing_count} losing BUY(s) open, limit {MAX_LOSING_POSITIONS})")
|
| 1031 |
+
res = place_market_order_smart(ssl_sock, account_id, symbol_id, ProtoOATradeSide.BUY, effective_volume, min_vol, step_vol)
|
| 1032 |
execution_msg = f"Opened BUY position for {ticker}. Order Result: {res.get('status')} {res.get('message', '')}"
|
| 1033 |
|
| 1034 |
elif action == "SELL":
|
|
|
|
| 1042 |
else:
|
| 1043 |
# Open new SELL position (existing positions stay untouched — only stop-trade/end-trade closes them)
|
| 1044 |
logger.info(f"Opening new Short position... ({losing_count} losing SELL(s) open, limit {MAX_LOSING_POSITIONS})")
|
| 1045 |
+
res = place_market_order_smart(ssl_sock, account_id, symbol_id, ProtoOATradeSide.SELL, effective_volume, min_vol, step_vol)
|
| 1046 |
execution_msg = f"Opened SELL position for {ticker}. Order Result: {res.get('status')} {res.get('message', '')}"
|
| 1047 |
|
| 1048 |
elif action == "CLOSE":
|
|
|
|
| 1094 |
else:
|
| 1095 |
execution_msg = f"No open positions on {ticker} to close."
|
| 1096 |
|
| 1097 |
+
elif action == "ANCHOR":
|
| 1098 |
+
# ANCHOR = consolidation detected. Open opposite trade, wait 5 sec, then close ALL.
|
| 1099 |
+
anchor_direction = data.get("direction", "").upper()
|
| 1100 |
+
|
| 1101 |
+
# Step 1: Open the opposite (hedging) trade
|
| 1102 |
+
if anchor_direction == "BUY":
|
| 1103 |
+
logger.info(f"ANCHOR: Opening hedging BUY on {ticker}...")
|
| 1104 |
+
res = place_market_order_smart(ssl_sock, account_id, symbol_id, ProtoOATradeSide.BUY, effective_volume, min_vol, step_vol)
|
| 1105 |
+
logger.info(f"ANCHOR: Hedging BUY opened. Result: {res.get('status')} {res.get('message', '')}")
|
| 1106 |
+
elif anchor_direction == "SELL":
|
| 1107 |
+
logger.info(f"ANCHOR: Opening hedging SELL on {ticker}...")
|
| 1108 |
+
res = place_market_order_smart(ssl_sock, account_id, symbol_id, ProtoOATradeSide.SELL, effective_volume, min_vol, step_vol)
|
| 1109 |
+
logger.info(f"ANCHOR: Hedging SELL opened. Result: {res.get('status')} {res.get('message', '')}")
|
| 1110 |
+
else:
|
| 1111 |
+
logger.warning(f"ANCHOR: No direction specified, skipping hedge trade.")
|
| 1112 |
+
|
| 1113 |
+
# Step 2: Wait 5 seconds for the hedge trade to fully settle
|
| 1114 |
+
ssl_sock.close()
|
| 1115 |
+
logger.info(f"ANCHOR: Waiting 5 seconds before closing all positions on {ticker}...")
|
| 1116 |
+
await asyncio.sleep(5)
|
| 1117 |
+
|
| 1118 |
+
# Step 3: Reconnect and close ALL positions on this symbol
|
| 1119 |
+
ssl_sock = get_authenticated_socket(account_id, access_token)
|
| 1120 |
+
symbol_id = resolve_symbol_id(ssl_sock, account_id, ticker)
|
| 1121 |
+
positions = get_open_positions(ssl_sock, account_id)
|
| 1122 |
+
symbol_positions = [p for p in positions if p.tradeData.symbolId == symbol_id]
|
| 1123 |
+
|
| 1124 |
+
closed_count = 0
|
| 1125 |
+
for p in symbol_positions:
|
| 1126 |
+
side = ProtoOATradeSide.SELL if p.tradeData.tradeSide == 1 else ProtoOATradeSide.BUY
|
| 1127 |
+
logger.info(f"ANCHOR: Closing {'BUY' if p.tradeData.tradeSide == 1 else 'SELL'} position {p.positionId}")
|
| 1128 |
+
place_market_order(ssl_sock, account_id, symbol_id, side, p.tradeData.volume, position_id=p.positionId)
|
| 1129 |
+
closed_count += 1
|
| 1130 |
+
|
| 1131 |
+
execution_msg = f"ANCHOR: Opened hedging {anchor_direction}, then closed {closed_count} position(s) on {ticker}. Consolidation neutralized."
|
| 1132 |
+
|
| 1133 |
ssl_sock.close()
|
| 1134 |
|
| 1135 |
# Log to in-memory dashboard logs
|