Update app.py
Browse files
app.py
CHANGED
|
@@ -861,13 +861,89 @@ async def webhook(request: Request):
|
|
| 861 |
res = place_market_order(ssl_sock, account_id, symbol_id, ProtoOATradeSide.SELL, current_trade_volume)
|
| 862 |
execution_msg = f"Opened SELL position for {ticker}. Order Result: {res.get('status')} {res.get('message', '')}"
|
| 863 |
|
| 864 |
-
elif action
|
| 865 |
-
#
|
| 866 |
if len(symbol_positions) > 0:
|
| 867 |
for p in symbol_positions:
|
| 868 |
side = ProtoOATradeSide.SELL if p.tradeData.tradeSide == 1 else ProtoOATradeSide.BUY
|
| 869 |
place_market_order(ssl_sock, account_id, symbol_id, side, p.tradeData.volume, position_id=p.positionId)
|
| 870 |
-
execution_msg = f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 871 |
else:
|
| 872 |
execution_msg = f"No open positions on {ticker} to close."
|
| 873 |
|
|
|
|
| 861 |
res = place_market_order(ssl_sock, account_id, symbol_id, ProtoOATradeSide.SELL, current_trade_volume)
|
| 862 |
execution_msg = f"Opened SELL position for {ticker}. Order Result: {res.get('status')} {res.get('message', '')}"
|
| 863 |
|
| 864 |
+
elif action == "CLOSE":
|
| 865 |
+
# CLOSE = force-close ALL positions on this symbol (manual override)
|
| 866 |
if len(symbol_positions) > 0:
|
| 867 |
for p in symbol_positions:
|
| 868 |
side = ProtoOATradeSide.SELL if p.tradeData.tradeSide == 1 else ProtoOATradeSide.BUY
|
| 869 |
place_market_order(ssl_sock, account_id, symbol_id, side, p.tradeData.volume, position_id=p.positionId)
|
| 870 |
+
execution_msg = f"CLOSE: Force-closed {len(symbol_positions)} positions on {ticker}."
|
| 871 |
+
else:
|
| 872 |
+
execution_msg = f"No open positions on {ticker} to close."
|
| 873 |
+
|
| 874 |
+
elif action == "STOP-TRADE":
|
| 875 |
+
# STOP-TRADE = only close PROFITABLE positions, leave losing ones open
|
| 876 |
+
if len(symbol_positions) > 0:
|
| 877 |
+
# Get current spot price (bid/ask) for this symbol
|
| 878 |
+
current_bid = None
|
| 879 |
+
current_ask = None
|
| 880 |
+
try:
|
| 881 |
+
spot_req = ProtoOASubscribeSpotsReq()
|
| 882 |
+
spot_req.ctidTraderAccountId = int(account_id)
|
| 883 |
+
spot_req.symbolId.append(int(symbol_id))
|
| 884 |
+
send_proto_message(ssl_sock, spot_req, 2127)
|
| 885 |
+
|
| 886 |
+
# Read responses until we get a spot event
|
| 887 |
+
for _ in range(10):
|
| 888 |
+
proto_msg = receive_proto_message(ssl_sock)
|
| 889 |
+
if not proto_msg:
|
| 890 |
+
break
|
| 891 |
+
if proto_msg.payloadType == 2131: # ProtoOASpotEvent
|
| 892 |
+
spot = ProtoOASpotEvent()
|
| 893 |
+
spot.ParseFromString(proto_msg.payload)
|
| 894 |
+
if hasattr(spot, 'bid') and spot.bid:
|
| 895 |
+
current_bid = spot.bid / 100000.0 # Convert from protocol price
|
| 896 |
+
if hasattr(spot, 'ask') and spot.ask:
|
| 897 |
+
current_ask = spot.ask / 100000.0
|
| 898 |
+
if current_bid and current_ask:
|
| 899 |
+
break
|
| 900 |
+
elif proto_msg.payloadType == 2128: # Subscribe confirmation
|
| 901 |
+
continue
|
| 902 |
+
|
| 903 |
+
# Unsubscribe from spots
|
| 904 |
+
try:
|
| 905 |
+
unsub_req = ProtoOAUnsubscribeSpotsReq()
|
| 906 |
+
unsub_req.ctidTraderAccountId = int(account_id)
|
| 907 |
+
unsub_req.symbolId.append(int(symbol_id))
|
| 908 |
+
send_proto_message(ssl_sock, unsub_req, 2129)
|
| 909 |
+
receive_proto_message(ssl_sock) # Consume the response
|
| 910 |
+
except Exception:
|
| 911 |
+
pass
|
| 912 |
+
except Exception as e:
|
| 913 |
+
logger.warning(f"Could not fetch spot price: {e}")
|
| 914 |
+
|
| 915 |
+
closed_count = 0
|
| 916 |
+
skipped_count = 0
|
| 917 |
+
|
| 918 |
+
for p in symbol_positions:
|
| 919 |
+
entry_price = p.price # Entry price of the position
|
| 920 |
+
is_buy = p.tradeData.tradeSide == 1
|
| 921 |
+
|
| 922 |
+
# Determine if position is profitable
|
| 923 |
+
in_profit = False
|
| 924 |
+
if current_bid and current_ask:
|
| 925 |
+
if is_buy:
|
| 926 |
+
# BUY position profits when current bid > entry price
|
| 927 |
+
in_profit = current_bid > entry_price
|
| 928 |
+
else:
|
| 929 |
+
# SELL position profits when entry price > current ask
|
| 930 |
+
in_profit = entry_price > current_ask
|
| 931 |
+
else:
|
| 932 |
+
# Fallback: use the swap field as a rough P&L indicator
|
| 933 |
+
# If we couldn't get spot prices, check if swap is positive
|
| 934 |
+
in_profit = getattr(p, 'swap', 0) > 0
|
| 935 |
+
logger.warning(f"No spot price available, using swap as fallback for position {p.positionId}")
|
| 936 |
+
|
| 937 |
+
if in_profit:
|
| 938 |
+
side = ProtoOATradeSide.SELL if is_buy else ProtoOATradeSide.BUY
|
| 939 |
+
logger.info(f"Closing PROFITABLE {'BUY' if is_buy else 'SELL'} position {p.positionId} (entry: {entry_price})")
|
| 940 |
+
place_market_order(ssl_sock, account_id, symbol_id, side, p.tradeData.volume, position_id=p.positionId)
|
| 941 |
+
closed_count += 1
|
| 942 |
+
else:
|
| 943 |
+
logger.info(f"KEEPING losing {'BUY' if is_buy else 'SELL'} position {p.positionId} (entry: {entry_price})")
|
| 944 |
+
skipped_count += 1
|
| 945 |
+
|
| 946 |
+
execution_msg = f"STOP-TRADE: Closed {closed_count} profitable position(s), kept {skipped_count} losing position(s) on {ticker}."
|
| 947 |
else:
|
| 948 |
execution_msg = f"No open positions on {ticker} to close."
|
| 949 |
|