From 40f6330a1340e051e4f929a92f5fdf7133d233d5 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Thu, 10 Nov 2022 07:20:14 +0000 Subject: [PATCH] Remove asset filter and begin implementing posting trades --- app/local_settings.py | 55 -------------------- core/exchanges/__init__.py | 9 +++- core/exchanges/oanda.py | 33 +++++++++--- core/lib/market.py | 43 +++++++++------- core/lib/schemas/oanda_s.py | 100 ++++++++++++++++++++++++++++++++++++ core/views/hooks.py | 4 -- core/views/positions.py | 6 ++- 7 files changed, 164 insertions(+), 86 deletions(-) diff --git a/app/local_settings.py b/app/local_settings.py index 476189b..561abc5 100644 --- a/app/local_settings.py +++ b/app/local_settings.py @@ -29,61 +29,6 @@ STRIPE_ADMIN_COUPON = getenv("STRIPE_ADMIN_COUPON", "") REGISTRATION_OPEN = getenv("REGISTRATION_OPEN", "false").lower() in trues -ASSET_FILTER = [ - "LINK/USDT", - "PAXG/USD", - "PAXG/USDT", - "SHIB/USD", - "TRX/USD", - "TRX/USDT", - "UNI/BTC", - "UNI/USD", - "UNI/USDT", - "USDT/USD", - "WBTC/USD", - "YFI/BTC", - "NEAR/USDT", - "SUSHI/USDT", - "DOGE/USDT", - "LINK/BTC", - "LINK/USD", - "GRT/USD", - "AVAX/BTC", - "AVAX/USD", - "AVAX/USDT", - "SOL/BTC", - "SOL/USD", - "SOL/USDT", - "BTC/USDT", - "SUSHI/BTC", - "SUSHI/USD", - "BCH/BTC", - "BCH/USD", - "YFI/USD", - "ETH/USD", - "ETH/USDT", - "YFI/USDT", - "AAVE/USD", - "AAVE/USDT", - "ALGO/USD", - "BAT/USD", - "DAI/USDT", - "ALGO/USDT", - "MATIC/BTC", - "MATIC/USD", - "DOGE/USD", - "MKR/USD", - "BTC/USD", - "DOGE/BTC", - "LTC/BTC", - "LTC/USD", - "LTC/USDT", - "ETH/BTC", - "BCH/USDT", - "DAI/USD", - "NEAR/USD", -] - # Hook URL, do not include leading or trailing slash HOOK_PATH = "hook" diff --git a/core/exchanges/__init__.py b/core/exchanges/__init__.py index 049e000..6bd742a 100644 --- a/core/exchanges/__init__.py +++ b/core/exchanges/__init__.py @@ -1,4 +1,6 @@ +from alpaca.common.exceptions import APIError from glom import glom +from oandapyV20.exceptions import V20Error from core.lib import schemas from core.util import logs @@ -135,7 +137,11 @@ class BaseExchange(object): :raises NoSchema: If the method is not in the schema mapping :raises ValidationError: If the response cannot be validated """ - response = self.call_method(method, *args, **kwargs) + try: + response = self.call_method(method, *args, **kwargs) + except (APIError, V20Error) as e: + log.error(f"Error calling method {method}: {e}") + raise GenericAPIError(e) try: response_valid = self.validate_response(response, method) except NoSchema as e: @@ -147,6 +153,7 @@ class BaseExchange(object): except NoSchema as e: log.error(f"{e} - {response}") response_converted = response_valid + # return (True, response_converted) return response_converted diff --git a/core/exchanges/oanda.py b/core/exchanges/oanda.py index 574003d..b8fdad5 100644 --- a/core/exchanges/oanda.py +++ b/core/exchanges/oanda.py @@ -1,5 +1,5 @@ from oandapyV20 import API -from oandapyV20.endpoints import accounts, positions +from oandapyV20.endpoints import accounts, orders, positions from core.exchanges import BaseExchange @@ -26,16 +26,37 @@ class OANDAExchange(BaseExchange): return [x["name"] for x in response["itemlist"]] def get_balance(self): - raise NotImplementedError + r = accounts.AccountSummary(self.account_id) + response = self.call(r) + return float(response["balance"]) def get_market_value(self, symbol): raise NotImplementedError def post_trade(self, trade): - raise NotImplementedError - # r = orders.OrderCreate(accountID, data=data) - # self.client.request(r) - # return r.response + if trade.direction == "sell": + amount = -trade.amount + else: + amount = trade.amount + data = { + "order": { + # "price": "1.5000", - added later + "stopLossOnFill": {"timeInForce": "GTC", "price": str(trade.stop_loss)}, + "takeProfitOnFill": {"price": str(trade.take_profit)}, + "timeInForce": "GTC", + "instrument": trade.symbol, + "units": str(amount), + "type": trade.type.upper(), + "positionFill": "DEFAULT", + } + } + print("SENDINGF ORDER", data) + if trade.type == "limit": + data["order"]["price"] = str(trade.price) + r = orders.OrderCreate(self.account_id, data=data) + response = self.call(r) + print("POSTED TRADE", response) + return response def get_trade(self, trade_id): r = accounts.TradeDetails(accountID=self.account_id, tradeID=trade_id) diff --git a/core/lib/market.py b/core/lib/market.py index dc56c42..c68904a 100644 --- a/core/lib/market.py +++ b/core/lib/market.py @@ -1,3 +1,5 @@ +from decimal import Decimal as D + from alpaca.common.exceptions import APIError from core.models import Strategy, Trade @@ -33,11 +35,14 @@ def execute_strategy(callback, strategy): base = callback.base quote = callback.quote direction = hook.direction - if quote not in ["usd", "usdt", "usdc", "busd"]: - log.error(f"Quote not compatible with Dollar: {quote}") - return False - quote = "usd" # TODO: MASSIVE HACK - symbol = f"{base.upper()}/{quote.upper()}" + if account.exchange == "alpaca": + if quote not in ["usd", "usdt", "usdc", "busd"]: + log.error(f"Quote not compatible with Dollar: {quote}") + return False + quote = "usd" # TODO: MASSIVE HACK + symbol = f"{base.upper()}/{quote.upper()}" + elif account.exchange == "oanda": + symbol = f"{base.upper()}_{quote.upper()}" if symbol not in account.supported_symbols: log.error(f"Symbol not supported by account: {symbol}") @@ -53,32 +58,32 @@ def execute_strategy(callback, strategy): # type = "limit" type = "market" - trade_size_as_ratio = strategy.trade_size_percent / 100 + trade_size_as_ratio = D(strategy.trade_size_percent) / D(100) log.debug(f"Trade size as ratio: {trade_size_as_ratio}") - amount_usd = trade_size_as_ratio * cash_balance + amount_usd = D(trade_size_as_ratio) * D(cash_balance) log.debug(f"Trade size: {amount_usd}") - price = callback.price + price = round(D(callback.price), 8) if not price: return log.debug(f"Extracted price of quote: {price}") # We can do this because the quote IS in $ or equivalent - trade_size_in_quote = amount_usd / price + trade_size_in_quote = D(amount_usd) / D(price) log.debug(f"Trade size in quote: {trade_size_in_quote}") # calculate sl/tp - stop_loss_as_ratio = strategy.stop_loss_percent / 100 - take_profit_as_ratio = strategy.take_profit_percent / 100 + stop_loss_as_ratio = D(strategy.stop_loss_percent) / D(100) + take_profit_as_ratio = D(strategy.take_profit_percent) / D(100) log.debug(f"Stop loss as ratio: {stop_loss_as_ratio}") log.debug(f"Take profit as ratio: {take_profit_as_ratio}") - stop_loss_subtract = price * stop_loss_as_ratio - take_profit_add = price * take_profit_as_ratio + stop_loss_subtract = D(price) * D(stop_loss_as_ratio) + take_profit_add = D(price) * D(take_profit_as_ratio) log.debug(f"Stop loss subtract: {stop_loss_subtract}") log.debug(f"Take profit add: {take_profit_add}") - stop_loss = price - stop_loss_subtract - take_profit = price + take_profit_add + stop_loss = D(price) - D(stop_loss_subtract) + take_profit = D(price) + D(take_profit_add) log.debug(f"Stop loss: {stop_loss}") log.debug(f"Take profit: {take_profit}") @@ -90,15 +95,15 @@ def execute_strategy(callback, strategy): symbol=symbol, type=type, # amount_usd=amount_usd, - amount=trade_size_in_quote, + amount=float(round(trade_size_in_quote, 2)), # price=price, - stop_loss=stop_loss, - take_profit=take_profit, + stop_loss=float(round(stop_loss, 2)), + take_profit=float(round(take_profit, 2)), direction=direction, ) new_trade.save() info = new_trade.post() - log.debug(f"Posted trade: {posted} - {info}") + log.debug(f"Posted trade: {info}") def process_callback(callback): diff --git a/core/lib/schemas/oanda_s.py b/core/lib/schemas/oanda_s.py index 0897783..c206040 100644 --- a/core/lib/schemas/oanda_s.py +++ b/core/lib/schemas/oanda_s.py @@ -179,6 +179,106 @@ AccountDetailsSchema = { } +{ + "account": { + "marginCloseoutNAV": "35454.4740", + "marginUsed": "10581.5000", + "currency": "EUR", + "resettablePL": "-13840.3525", + "NAV": "35454.4740", + "marginCloseoutMarginUsed": "10581.5000", + "marginCloseoutPositionValue": "211630.0000", + "openTradeCount": 2, + "id": "101-004-1435156-001", + "hedgingEnabled": False, + "marginCloseoutPercent": "0.14923", + "marginCallMarginUsed": "10581.5000", + "openPositionCount": 1, + "positionValue": "211630.0000", + "pl": "-13840.3525", + "lastTransactionID": "2123", + "marginAvailable": "24872.9740", + "marginRate": "0.05", + "marginCallPercent": "0.29845", + "pendingOrderCount": 0, + "withdrawalLimit": "24872.9740", + "unrealizedPL": "0.0000", + "alias": "hootnotv20", + "createdByUserID": 1435156, + "marginCloseoutUnrealizedPL": "0.0000", + "createdTime": "2016-06-24T21:03:50.914647476Z", + "balance": "35454.4740", + }, + "lastTransactionID": "2123", +} + + +class AccountSummaryNested(BaseModel): + marginCloseoutNAV: str + marginUsed: str + currency: str + resettablePL: str + NAV: str + marginCloseoutMarginUsed: str + marginCloseoutPositionValue: str + openTradeCount: int + id: str + hedgingEnabled: bool + marginCloseoutPercent: str + marginCallMarginUsed: str + openPositionCount: int + positionValue: str + pl: str + lastTransactionID: str + marginAvailable: str + marginRate: str + marginCallPercent: str + pendingOrderCount: int + withdrawalLimit: str + unrealizedPL: str + alias: str + createdByUserID: int + marginCloseoutUnrealizedPL: str + createdTime: str + balance: str + + +class AccountSummary(BaseModel): + account: AccountSummaryNested + lastTransactionID: str + + +AccountSummarySchema = { + "marginCloseoutNAV": "account.marginCloseoutNAV", + "marginUsed": "account.marginUsed", + "currency": "account.currency", + "resettablePL": "account.resettablePL", + "NAV": "account.NAV", + "marginCloseoutMarginUsed": "account.marginCloseoutMarginUsed", + "marginCloseoutPositionValue": "account.marginCloseoutPositionValue", + "openTradeCount": "account.openTradeCount", + "id": "account.id", + "hedgingEnabled": "account.hedgingEnabled", + "marginCloseoutPercent": "account.marginCloseoutPercent", + "marginCallMarginUsed": "account.marginCallMarginUsed", + "openPositionCount": "account.openPositionCount", + "positionValue": "account.positionValue", + "pl": "account.pl", + "lastTransactionID": "account.lastTransactionID", + "marginAvailable": "account.marginAvailable", + "marginRate": "account.marginRate", + "marginCallPercent": "account.marginCallPercent", + "pendingOrderCount": "account.pendingOrderCount", + "withdrawalLimit": "account.withdrawalLimit", + "unrealizedPL": "account.unrealizedPL", + "alias": "account.alias", + "createdByUserID": "account.createdByUserID", + "marginCloseoutUnrealizedPL": "account.marginCloseoutUnrealizedPL", + "createdTime": "account.createdTime", + "balance": "account.balance", +} + + class PositionDetailsNested(BaseModel): instrument: str long: PositionLong diff --git a/core/views/hooks.py b/core/views/hooks.py index 43d6bd4..441c84b 100644 --- a/core/views/hooks.py +++ b/core/views/hooks.py @@ -1,7 +1,6 @@ import re import orjson -from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponse, HttpResponseBadRequest from pydantic import ValidationError @@ -56,9 +55,6 @@ class HookAPI(APIView): base = hook_resp.market.item quote = hook_resp.market.currency symbol = f"{base.upper()}/{quote.upper()}" - if symbol not in settings.ASSET_FILTER: - log.debug(f"Skipping {symbol} because it is not in the asset filter") - return HttpResponseBadRequest("Invalid symbol") data = { "title": hook_resp.title, diff --git a/core/views/positions.py b/core/views/positions.py index 9f70b95..7bdfdf0 100644 --- a/core/views/positions.py +++ b/core/views/positions.py @@ -6,6 +6,7 @@ from django.shortcuts import render from django.views import View from rest_framework.parsers import FormParser +from core.exchanges import GenericAPIError from core.models import Account from core.util import logs @@ -16,7 +17,10 @@ def get_positions(user, account_id=None): items = [] accounts = Account.objects.filter(user=user) for account in accounts: - positions = account.client.get_all_positions() + try: + positions = account.client.get_all_positions() + except GenericAPIError: + continue for item in positions: items.append(item)