Skip to content

Commit

Permalink
Rebalancing endpoints (#362)
Browse files Browse the repository at this point in the history
* feat: draft rebalancing create portfolio

* feat: get all portfolios and create subscription

* feat: get portfolio by ID

* feat: update portfolio by ID

* feat: inactivate portfolio by ID

* feat: get all subscriptions

* feat: get subscription by ID

* feat: delete subscription by ID

* feat: manual run endpoint

* feat: list all runs with page iteration

* feat: get and cancel run endpoints

* fix: pagination like other endpoints

* fix: missing client docstrings

* chore: adjust typing

* chore: black

* feat: adjust rebalancing

---------

Co-authored-by: Chihiro Hio <[email protected]>
  • Loading branch information
alessiocastrica and hiohiohio authored Oct 7, 2024
1 parent ff70877 commit 9e6dcd9
Show file tree
Hide file tree
Showing 8 changed files with 1,387 additions and 13 deletions.
395 changes: 393 additions & 2 deletions alpaca/broker/client.py

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions alpaca/broker/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,100 @@ class JournalStatus(str, Enum):
REFUSED = "refused"
CORRECT = "correct"
DELETED = "deleted"


class PortfolioStatus(str, Enum):
"""
The possible values of the Portfolio status.
See https://docs.alpaca.markets/reference/get-v1-rebalancing-portfolios
"""

ACTIVE = "active"
INACTIVE = "inactive"
NEEDS_ADJUSTMENT = "needs_adjustment"


class WeightType(str, Enum):
"""
The possible values of the Weight type.
See https://docs.alpaca.markets/reference/post-v1-rebalancing-portfolios
"""

CASH = "cash"
ASSET = "asset"


class RebalancingConditionsType(str, Enum):
"""
The possible values of the Rebalancing Conditions type.
See https://docs.alpaca.markets/reference/post-v1-rebalancing-portfolios
"""

DRIFT_BAND = "drift_band"
CALENDAR = "calendar"


class DriftBandSubType(str, Enum):
"""
The possible values of the Rebalancing Conditions subtype for drift_band.
See https://docs.alpaca.markets/reference/post-v1-rebalancing-portfolios
"""

ABSOLUTE = "absolute"
RELATIVE = "relative"


class CalendarSubType(str, Enum):
"""
The possible values of the Rebalancing Conditions subtype for drift_band.
See https://docs.alpaca.markets/reference/post-v1-rebalancing-portfolios
"""

WEEKLY = "weekly"
MONTHLY = "monthly"
QUARTERLY = "quarterly"
ANNUALLY = "annually"


class RunType(str, Enum):
"""
The possible values of the Run type.
See https://docs.alpaca.markets/reference/post-v1-rebalancing-runs
"""

FULL_REBALANCE = "full_rebalance"
INVEST_CASH = "invest_cash"


class RunInitiatedFrom(str, Enum):
"""
The possible values of the initiated_from field.
See https://docs.alpaca.markets/docs/portfolio-rebalancing
"""

SYSTEM = "system"
API = "api"


class RunStatus(str, Enum):
"""
The possible values of the Run status.
See https://docs.alpaca.markets/reference/get-v1-rebalancing-runs
"""

QUEUED = "QUEUED"
IN_PROGRESS = "IN_PROGRESS"
CANCELED = "CANCELED"
CANCELED_MID_RUN = "CANCELED_MID_RUN"
ERROR = "ERROR"
TIMEOUT = "TIMEOUT"
COMPLETED_SUCCESS = "COMPLETED_SUCCESS"
COMPLETED_ADJUSTED = "COMPLETED_ADJUSTED"
1 change: 1 addition & 0 deletions alpaca/broker/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from .funding import *
from .trading import *
from .journals import *
from .rebalancing import *
80 changes: 80 additions & 0 deletions alpaca/broker/models/rebalancing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from datetime import datetime
from typing import List, Optional
from uuid import UUID

from alpaca.broker.enums import PortfolioStatus, RunInitiatedFrom, RunStatus, RunType
from alpaca.broker.models import Order
from alpaca.broker.requests import RebalancingConditions, Weight
from alpaca.common.models import ValidateBaseModel as BaseModel


class Portfolio(BaseModel):
"""
Portfolio response model.
https://docs.alpaca.markets/reference/get-v1-rebalancing-portfolios
"""

id: UUID
name: str
description: str
status: PortfolioStatus
cooldown_days: int
created_at: datetime
updated_at: datetime
weights: List[Weight]
rebalance_conditions: Optional[List[RebalancingConditions]] = None


class Subscription(BaseModel):
"""
Subscription response model.
https://docs.alpaca.markets/reference/get-v1-rebalancing-subscriptions-1
"""

id: UUID
account_id: UUID
portfolio_id: UUID
created_at: datetime
last_rebalanced_at: Optional[datetime] = None


class SkippedOrder(BaseModel):
"""
Skipped order response model.
https://docs.alpaca.markets/reference/get-v1-rebalancing-runs-run_id-1
"""

symbol: str
side: Optional[str] = None
notional: Optional[str] = None
currency: Optional[str] = None
reason: str
reason_details: str


class RebalancingRun(BaseModel):
"""
Rebalancing run response model.
https://docs.alpaca.markets/reference/get-v1-rebalancing-runs
"""

id: UUID
account_id: UUID
type: RunType
amount: Optional[str] = None
portfolio_id: UUID
weights: List[Weight]
initiated_from: Optional[RunInitiatedFrom] = None
created_at: datetime
updated_at: datetime
completed_at: Optional[datetime] = None
canceled_at: Optional[datetime] = None
status: RunStatus
reason: Optional[str] = None
orders: Optional[List[Order]] = None
failed_orders: Optional[List[Order]] = None
skipped_orders: Optional[List[SkippedOrder]] = None
153 changes: 146 additions & 7 deletions alpaca/broker/requests.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from datetime import date, datetime
from typing import List, Optional, Union, Dict, Any
from typing import Any, Dict, List, Optional, Union
from uuid import UUID

from pydantic import model_validator, field_validator
from pydantic import field_validator, model_validator

from alpaca.broker.models.accounts import (
AccountDocument,
Expand All @@ -16,22 +16,27 @@
from alpaca.broker.enums import (
AccountEntities,
BankAccountType,
CalendarSubType,
DocumentType,
EmploymentStatus,
DriftBandSubType,
FeePaymentMethod,
FundingSource,
IdentifierType,
TaxIdType,
JournalEntryType,
JournalStatus,
PortfolioStatus,
RebalancingConditionsType,
RunType,
TradeDocumentType,
TransferDirection,
TransferTiming,
TransferType,
UploadDocumentMimeType,
UploadDocumentSubType,
VisaType,
JournalEntryType,
JournalStatus,
WeightType,
)
from alpaca.common.models import BaseModel
from alpaca.common.enums import Sort, SupportedCurrencies
from alpaca.trading.enums import ActivityType, AccountStatus, OrderType, AssetClass
from alpaca.common.requests import NonEmptyRequest
Expand All @@ -44,7 +49,6 @@
TrailingStopOrderRequest as BaseTrailingStopOrderRequest,
)


# ############################## Accounts ################################# #


Expand Down Expand Up @@ -985,3 +989,138 @@ class GetEventsRequest(NonEmptyRequest):
until: Optional[Union[date, str]] = None
since_id: Optional[int] = None
until_id: Optional[int] = None


# ############################## Rebalancing ################################# #


class Weight(BaseModel):
"""
Weight model.
https://docs.alpaca.markets/reference/post-v1-rebalancing-portfolios
"""

type: WeightType
symbol: Optional[str] = None
percent: float

@field_validator("percent")
def percent_must_be_positive(cls, value: float) -> float:
"""Validate and round the percent field to 2 decimal places."""
if value <= 0:
raise ValueError("You must provide an amount > 0.")
return round(value, 2)

@model_validator(mode="before")
def validator(cls, values: dict) -> dict:
"""Verify that the symbol is provided when the weights type is asset."""
if (
values["type"] == WeightType.ASSET.value
and values.get("symbol", None) is None
):
raise ValueError
return values


class RebalancingConditions(BaseModel):
"""
Rebalancing conditions model.
https://docs.alpaca.markets/reference/post-v1-rebalancing-portfolios
"""

type: RebalancingConditionsType
sub_type: Union[DriftBandSubType, CalendarSubType]
percent: Optional[float] = None
day: Optional[str] = None


class CreatePortfolioRequest(NonEmptyRequest):
"""
Portfolio request model.
https://docs.alpaca.markets/reference/post-v1-rebalancing-portfolios
"""

name: str
description: str
weights: List[Weight]
cooldown_days: int
rebalance_conditions: Optional[List[RebalancingConditions]] = None


class UpdatePortfolioRequest(NonEmptyRequest):
"""
Portfolio request update model.
https://docs.alpaca.markets/reference/patch-v1-rebalancing-portfolios-portfolio_id-1
"""

name: Optional[str] = None
description: Optional[str] = None
weights: Optional[List[Weight]] = None
cooldown_days: Optional[int] = None
rebalance_conditions: Optional[List[RebalancingConditions]] = None


class GetPortfoliosRequest(NonEmptyRequest):
"""
Get portfolios request query parameters.
https://docs.alpaca.markets/reference/get-v1-rebalancing-portfolios
"""

name: Optional[str] = None
description: Optional[str] = None
symbol: Optional[str] = None
portfolio_id: Optional[UUID] = None
status: Optional[PortfolioStatus] = None


class CreateSubscriptionRequest(NonEmptyRequest):
"""
Subscription request model.
https://docs.alpaca.markets/reference/post-v1-rebalancing-subscriptions-1
"""

account_id: UUID
portfolio_id: UUID


class GetSubscriptionsRequest(NonEmptyRequest):
"""
Get subscriptions request query parameters.
https://docs.alpaca.markets/reference/get-v1-rebalancing-subscriptions-1
"""

account_id: Optional[UUID] = None
portfolio_id: Optional[UUID] = None
limit: Optional[int] = None
page_token: Optional[str] = None


class CreateRunRequest(NonEmptyRequest):
"""
Manually creates a rebalancing run.
https://docs.alpaca.markets/reference/post-v1-rebalancing-runs
"""

account_id: UUID
type: RunType
weights: List[Weight]


class GetRunsRequest(NonEmptyRequest):
"""
Get runs request query parameters.
https://docs.alpaca.markets/reference/get-v1-rebalancing-runs
"""

account_id: Optional[UUID] = None
type: Optional[RunType] = None
limit: Optional[int] = None
6 changes: 4 additions & 2 deletions alpaca/common/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __init__(
"""

self._api_key, self._secret_key, self._oauth_token = self._validate_credentials(
api_key, secret_key, oauth_token
api_key=api_key, secret_key=secret_key, oauth_token=oauth_token
)
self._api_version: str = api_version
self._base_url: Union[BaseURL, str] = base_url
Expand Down Expand Up @@ -207,7 +207,9 @@ def _one_request(self, method: str, url: str, opts: dict, retry: int) -> dict:
if response.text != "":
return response.json()

def get(self, path: str, data: Union[dict, str] = None, **kwargs) -> HTTPResult:
def get(
self, path: str, data: Optional[Union[dict, str]] = None, **kwargs
) -> HTTPResult:
"""Performs a single GET request
Args:
Expand Down
Loading

0 comments on commit 9e6dcd9

Please sign in to comment.