diff --git a/core/tests/lib/test_market.py b/core/tests/lib/test_market.py index 4b5efd3..696345a 100644 --- a/core/tests/lib/test_market.py +++ b/core/tests/lib/test_market.py @@ -1,6 +1,6 @@ from django.test import TestCase -from core.lib.market import check_conflicting_position, check_existing_position +from core.trading.crossfilter import check_conflicting_position, check_existing_position class MarketTestCase(TestCase): diff --git a/core/trading/__init__.py b/core/trading/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/trading/crossfilter.py b/core/trading/crossfilter.py new file mode 100644 index 0000000..8e3006a --- /dev/null +++ b/core/trading/crossfilter.py @@ -0,0 +1,161 @@ +from core.exchanges import GenericAPIError +from core.util import logs + +log = logs.get_logger(__name__) + + +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 + with an opposite direction to this one. + :param account: Account object + :param symbol: Symbol + :param direction: Direction of the trade + :param func: Whether we are checking entries or exits + :return: dict of action and opposing position, or False + """ + try: + # 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") + return False + else: + log.error(f"Error getting position info: {e}") + return None + if new_direction == "buy": + opposing_side = "short" + 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 + + return False diff --git a/core/lib/market.py b/core/trading/market.py similarity index 76% rename from core/lib/market.py rename to core/trading/market.py index aba0147..c005e17 100644 --- a/core/lib/market.py +++ b/core/trading/market.py @@ -4,168 +4,12 @@ from decimal import Decimal as D from core.exchanges import GenericAPIError from core.lib.notify import sendmsg from core.models import Account, Strategy, Trade +from core.trading.crossfilter import crossfilter from core.util import logs log = logs.get_logger(__name__) -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 - with an opposite direction to this one. - :param account: Account object - :param symbol: Symbol - :param direction: Direction of the trade - :param func: Whether we are checking entries or exits - :return: dict of action and opposing position, or False - """ - try: - # 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") - return False - else: - log.error(f"Error getting position info: {e}") - return None - if new_direction == "buy": - opposing_side = "short" - 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 - - return False - - def get_pair(account, base, quote, invert=False): """ Get the pair for the given account and currencies. diff --git a/core/views/hooks.py b/core/views/hooks.py index 7291c0e..567ee24 100644 --- a/core/views/hooks.py +++ b/core/views/hooks.py @@ -9,9 +9,9 @@ from rest_framework.parsers import JSONParser from rest_framework.views import APIView from core.forms import HookForm -from core.lib import market from core.lib.schemas.drakdoo_s import DrakdooCallback from core.models import Callback, Hook, Signal +from core.trading import market from core.util import logs from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate