diff --git a/app/urls.py b/app/urls.py index 327d240..34f7a35 100644 --- a/app/urls.py +++ b/app/urls.py @@ -143,6 +143,11 @@ urlpatterns = [ positions.Positions.as_view(), name="positions", ), + path( + "positions/close////", + positions.PositionAction.as_view(), + name="position_action", + ), path( "positions////", positions.PositionAction.as_view(), diff --git a/core/exchanges/__init__.py b/core/exchanges/__init__.py index f7182e4..2efa4b5 100644 --- a/core/exchanges/__init__.py +++ b/core/exchanges/__init__.py @@ -218,6 +218,10 @@ class BaseExchange(ABC): def get_all_positions(self): pass + @abstractmethod + def close_position(self, side, symbol): + pass + @abstractmethod def close_all_positions(self): pass diff --git a/core/exchanges/alpaca.py b/core/exchanges/alpaca.py index 07a3e24..49b2b8d 100644 --- a/core/exchanges/alpaca.py +++ b/core/exchanges/alpaca.py @@ -130,3 +130,9 @@ class AlpacaExchange(BaseExchange): item["unrealized_pl"] = float(item["unrealized_pl"]) items.append(item) return items + + def close_position(self, side, symbol): + pass + + def close_all_positions(self): + pass diff --git a/core/lib/schemas/oanda_s.py b/core/lib/schemas/oanda_s.py index 40e5a07..54d55df 100644 --- a/core/lib/schemas/oanda_s.py +++ b/core/lib/schemas/oanda_s.py @@ -518,7 +518,7 @@ class LongOrderFillTransaction(BaseModel): longPositionCloseout: LongPositionCloseout -class OrderTransation(BaseModel): +class OrderTransaction(BaseModel): id: str accountID: str userID: int @@ -531,8 +531,8 @@ class OrderTransation(BaseModel): timeInForce: str | None positionFill: str | None reason: str - longPositionCloseout: LongPositionCloseout - longOrderFillTransaction: dict + longPositionCloseout: LongPositionCloseout | None + longOrderFillTransaction: dict | None class PositionClose(BaseModel): @@ -578,7 +578,7 @@ class TradeDetailsTrade(BaseModel): dividendAdjustment: str closeTime: str averageClosePrice: str - clientExtensions: ClientExtensions + clientExtensions: ClientExtensions | None class TradeDetails(BaseModel): diff --git a/core/views/positions.py b/core/views/positions.py index 5b8a0f7..58db715 100644 --- a/core/views/positions.py +++ b/core/views/positions.py @@ -3,6 +3,7 @@ import uuid from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseBadRequest from django.shortcuts import render +from django.urls import reverse from django.views import View from rest_framework.parsers import FormParser from two_factor.views.mixins import OTPRequiredMixin @@ -64,12 +65,20 @@ class Positions(LoginRequiredMixin, OTPRequiredMixin, View): def get(self, request, type, account_id=None): if type not in self.allowed_types: return HttpResponseBadRequest - template_name = f"wm/{type}.html" + self.template_name = f"wm/{type}.html" unique = str(uuid.uuid4())[:8] items = get_positions(request.user, account_id) annotate_positions(items, request.user, return_order_ids=False) + + orig_type = type if type == "page": type = "modal" + cast = { + "type": orig_type, + } + if account_id: + cast["account_id"] = account_id + list_url = reverse("positions", kwargs={**cast}) context = { "title": f"Positions ({type})", "unique": unique, @@ -79,8 +88,17 @@ class Positions(LoginRequiredMixin, OTPRequiredMixin, View): "type": type, "page_title": self.page_title, "page_subtitle": self.page_subtitle, + "list_url": list_url, + "context_object_name_singular": "position", + "context_object_name": "positions", } - return render(request, template_name, context) + # Return partials for HTMX + if self.request.htmx: + if orig_type == "page": + self.template_name = self.list_template + else: + context["window_content"] = self.list_template + return render(request, self.template_name, context) class PositionAction(LoginRequiredMixin, OTPRequiredMixin, View): @@ -103,6 +121,10 @@ class PositionAction(LoginRequiredMixin, OTPRequiredMixin, View): annotate_positions([info], request.user, return_order_ids=True) ) + # Remove some fields from the info dict + del info["long"] + del info["short"] + if type == "page": type = "modal" context = { @@ -115,3 +137,28 @@ class PositionAction(LoginRequiredMixin, OTPRequiredMixin, View): } return render(request, template_name, context) + + def delete(self, request, account_id, side, symbol): + """ + Close a position. + """ + template_name = "partials/notify.html" + account = Account.get_by_id(account_id, request.user) + try: + api_response = account.client.close_position(side, symbol) + except GenericAPIError as e: + context = {"message": e, "class": "danger"} + return render(request, template_name, context) + if "longOrderCreateTransaction" in api_response: + context = { + "message": f"Long position closed on {symbol}", + "class": "success", + } + elif "shortOrderCreateTransaction" in api_response: + context = { + "message": f"Short position closed on {symbol}", + "class": "success", + } + response = render(request, template_name, context) + response["HX-Trigger"] = "positionEvent" + return response