From 1793b5cc5d75e7bd8254cac9ec6133dab72b4793 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Mon, 12 Dec 2022 19:53:20 +0000 Subject: [PATCH] Prevent betting against ourselves via inverted pairs --- core/lib/market.py | 154 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 133 insertions(+), 21 deletions(-) diff --git a/core/lib/market.py b/core/lib/market.py index be55925..8c4ec6c 100644 --- a/core/lib/market.py +++ b/core/lib/market.py @@ -8,7 +8,82 @@ from core.util import logs log = logs.get_logger(__name__) -def crossfilter(account, symbol, direction, func): +def check_existing_position( + func: str, + position: dict, + open_side: str, + open_symbol: str, + open_units: str, + new_side: str, + new_symbol: str, + trade_side_opposite: str, +): + # Check if we already have a position for the symbol + if open_symbol == new_symbol: + # If the live side is the inverse of what we want to do, + # we can't open a position + if open_side == trade_side_opposite: + # If there is a position open, we can't open a new one in the opposite + # direction + if open_units != "0": + # If we have a short on GBP/AUD, we can only place more shorts on + # GBP/AUD. + if func == "entry": + log.debug( + f"Refusing to open new {new_side} position on {new_symbol} due " + f"to {open_side} position on {open_symbol}" + ) + return { + "action": "rejected", + "positions": position, + } + elif func == "exit": + log.debug( + ( + f"Found {open_units} units of " + f"{open_symbol} on side {trade_side_opposite}" + ) + ) + # Pass back opposing side so we can close it + return { + "action": "close", + "side": trade_side_opposite, + "positions": position, + } + return False + + +def check_conflicting_position( + func: str, + position: dict, + open_base: str, + open_quote: str, + open_side: str, + open_symbol: str, + open_units: str, + new_base: str, + new_quote: str, + new_side: str, + new_symbol: str, + trade_side_opposite: str, +): + if open_base == new_quote or open_quote == new_base: + # If we have a long on GBP/AUD, we can only place shorts on XAU/GBP. + if open_side != trade_side_opposite: + if open_units != "0": + # Only do this for entries + if func == "entry": + log.debug( + f"Refusing to open {new_side} position on {new_symbol} due to " + f"{open_side} position on {open_symbol}" + ) + return { + "action": "rejected", + "positions": position, + } + + +def crossfilter(account, new_symbol, new_direction, func): """ Determine if we are betting against ourselves. Checks open positions for the account, rejecting the trade if there is one @@ -20,7 +95,11 @@ def crossfilter(account, symbol, direction, func): :return: dict of action and opposing position, or False """ try: - position_info = account.client.get_position_info(symbol) + # Only get the data we need + if func == "entry": + all_positions = account.client.get_all_positions() + else: + all_positions = [account.client.get_position_info(new_symbol)] except GenericAPIError as e: if "No position exists for the specified instrument" in str(e): log.debug("No position exists for this symbol") @@ -28,28 +107,61 @@ def crossfilter(account, symbol, direction, func): else: log.error(f"Error getting position info: {e}") return None - if direction == "buy": + if new_direction == "buy": opposing_side = "short" - elif direction == "sell": + new_side = "long" + elif new_direction == "sell": opposing_side = "long" + new_side = "short" + + quotes = [] + new_base, new_quote = new_symbol.split("_") + for position in all_positions: + # For Forex, get a list of all the quotes. + # This is to prevent betting against ourselves. + # Consider we have a long position open, EUR/USD, and we want to open a + # long position on USD/JPY. If the first goes up, the second one will go + # down just as much. We won't make any money. + if "_" in position["symbol"]: + open_base, open_quote = position["symbol"].split("_") + quotes.append(open_quote) + + open_symbol = position["symbol"] + open_side = position["side"] + open_base, open_quote = open_symbol.split("_") + + # Check if we already have a position + existing_position_check = check_existing_position( + func=func, + position=position, + open_side=open_side, + open_symbol=open_symbol, + open_units=position["units"], + new_side=new_side, + new_symbol=new_symbol, + trade_side_opposite=opposing_side, + ) + if existing_position_check: + return existing_position_check + + # Check if we are betting against ourselves + conflicting_position_check = check_conflicting_position( + func=func, + position=position, + open_base=open_base, + open_quote=open_quote, + open_side=open_side, + open_symbol=open_symbol, + open_units=position["units"], + new_base=new_base, + new_quote=new_quote, + new_side=new_side, + new_symbol=new_symbol, + trade_side_opposite=opposing_side, + ) + if conflicting_position_check: + return conflicting_position_check - opposing_position_info = position_info[opposing_side] - if opposing_position_info["units"] != "0": - if func == "entry": - return {"action": "rejected", "positions": opposing_position_info} - elif func == "exit": - log.debug( - ( - f"Found {opposing_position_info['units']} units of " - f"{symbol} on side {opposing_side}" - ) - ) - # Pass back opposing side so we can close it - return { - "action": "close", - "side": opposing_side, - "positions": opposing_position_info, - } return False