diff --git a/core/lib/market.py b/core/lib/market.py index 102042e..83feab2 100644 --- a/core/lib/market.py +++ b/core/lib/market.py @@ -8,6 +8,37 @@ from core.util import logs log = logs.get_logger(__name__) +def crossfilter(account, symbol, 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 + """ + position_info = account.client.get_position_info(symbol) + if direction == "buy": + opposing_side = "short" + elif direction == "sell": + opposing_side = "long" + + 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": + # Pass back opposing side so we can close it + return { + "action": "close", + "side": opposing_side, + "positions": opposing_position_info, + } + return False + + def get_pair(account, base, quote, invert=False): """ Get the pair for the given account and currencies. @@ -258,27 +289,25 @@ def get_price_bound(direction, strategy, price, current_price): return price_bound -def execute_strategy(callback, strategy): +def execute_strategy(callback, strategy, func): """ Execute a strategy. :param callback: Callback object :param strategy: Strategy object """ - # Check if we can trade now! - now_utc = datetime.utcnow() - trading_times = strategy.trading_times.all() - if not trading_times: - log.error("No trading times set for strategy") - return - matches = [x.within_range(now_utc) for x in trading_times] - if not any(matches): - log.debug("Not within trading time range") - return - - # Get the account's balance in the native account currency - cash_balance = strategy.account.client.get_balance() - log.debug(f"Cash balance: {cash_balance}") + # Only check times for entries. We can always exit trades. + if func == "entry": + # Check if we can trade now! + now_utc = datetime.utcnow() + trading_times = strategy.trading_times.all() + if not trading_times: + log.error("No trading times set for strategy") + return + matches = [x.within_range(now_utc) for x in trading_times] + if not any(matches): + log.debug("Not within trading time range") + return # Instruments supported by the account if not strategy.account.instruments: @@ -329,12 +358,34 @@ def execute_strategy(callback, strategy): price = round(D(callback.price), display_precision) log.debug(f"Extracted price of quote: {price}") - type = strategy.order_type - current_price = get_price(account, direction, symbol) log.debug(f"Callback price: {price}") log.debug(f"Current price: {current_price}") + # Calculate price bound and round to the display precision + price_bound = get_price_bound(direction, strategy, price, current_price) + if not price_bound: + return + price_bound = round(price_bound, display_precision) + + # Callback now verified + if func == "exit": + check_exit = crossfilter(account, symbol, direction, func) + if not check_exit: + log.debug("Exit conditions not met.") + return + if check_exit["action"] == "close": + log.debug(f"Closing position on exit signal: {symbol}") + side = check_exit["side"] + response = account.client.close_position(side, symbol) + log.debug(f"Close position response: {response}") + return + type = strategy.order_type + + # Get the account's balance in the native account currency + cash_balance = strategy.account.client.get_balance() + log.debug(f"Cash balance: {cash_balance}") + # Convert the trade size, which is currently in the account's base currency, # to the base currency of the pair we are trading trade_size_in_base = get_trade_size_in_base( @@ -349,12 +400,6 @@ def execute_strategy(callback, strategy): if "tsl" in protection: trailing_stop_loss = protection["tsl"] - # Calculate price bound and round to the display precision - price_bound = get_price_bound(direction, strategy, price, current_price) - if not price_bound: - return - price_bound = round(price_bound, display_precision) - # Create object, note that the amount is rounded to the trade precision new_trade = Trade.objects.create( user=user, @@ -378,8 +423,21 @@ def execute_strategy(callback, strategy): round(trailing_stop_loss, display_precision) ) new_trade.save() - info = new_trade.post() - log.debug(f"Posted trade: {info}") + + # Run the crossfilter to ensure we don't trade the same pair in opposite directions + filtered = crossfilter(account, symbol, direction, func) + + # TP/SL calculation and get_trade_size_in_base are wasted here, but it's important + # to record the decision in the Trade object. We can only get it after we do those. + # It shows what would be done. + if filtered: + log.debug(f"Trade filtered. Action: {filtered['action']}") + if filtered["action"] == "rejected": + new_trade.status = "rejected" + new_trade.save() + else: + info = new_trade.post() + log.debug(f"Posted trade: {info}") def process_callback(callback): @@ -394,7 +452,7 @@ def process_callback(callback): if callback.hook.user != strategy.user: log.error("Ownership differs between callback and strategy.") continue - execute_strategy(callback, strategy) + execute_strategy(callback, strategy, func="entry") # Scan for exit log.debug("Scanning for entry strategies...") @@ -405,4 +463,4 @@ def process_callback(callback): if callback.hook.user != strategy.user: log.error("Ownership differs between callback and strategy.") continue - execute_strategy(callback, strategy) + execute_strategy(callback, strategy, func="exit")