Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import structlog
from alpaca.common.exceptions import APIError
from alpaca.data import StockHistoricalDataClient
from alpaca.data.requests import StockLatestQuoteRequest
from alpaca.trading import (
ClosePositionRequest,
OrderRequest,
Expand Down Expand Up @@ -61,82 +60,26 @@ def get_account(self) -> AlpacaAccount:
buying_power=float(cast("str", account.buying_power)),
)

def _get_current_price(self, ticker: str, side: TradeSide) -> float:
"""Get current price for a ticker based on trade side.

Uses ask price for buys, bid price for sells.
Falls back to the opposite price if the primary price is unavailable.
"""
request = StockLatestQuoteRequest(symbol_or_symbols=ticker.upper())
quotes = self.data_client.get_stock_latest_quote(request)
quote = quotes.get(ticker.upper())

if quote is None:
message = f"No quote returned for {ticker}"
raise ValueError(message)

ask_price = (
float(quote.ask_price)
if quote.ask_price is not None and quote.ask_price > 0
else 0.0
)
bid_price = (
float(quote.bid_price)
if quote.bid_price is not None and quote.bid_price > 0
else 0.0
)

if side == TradeSide.BUY:
if ask_price > 0:
return ask_price
if bid_price > 0:
logger.warning(
"Ask price unavailable, using bid price as fallback",
ticker=ticker,
side=side.value,
bid_price=bid_price,
)
return bid_price
message = f"No valid price for {ticker}: ask and bid are 0"
raise ValueError(message)

if bid_price > 0:
return bid_price
if ask_price > 0:
logger.warning(
"Bid price unavailable, using ask price as fallback",
ticker=ticker,
side=side.value,
ask_price=ask_price,
)
return ask_price
message = f"No valid price for {ticker}: bid and ask are 0"
raise ValueError(message)

def open_position(
self,
ticker: str,
side: TradeSide,
dollar_amount: float,
) -> None:
# Calculate quantity from dollar amount and current price
# Allow fractional shares where supported by the brokerage
current_price = self._get_current_price(ticker, side)
qty = dollar_amount / current_price

if qty <= 0:
# Use notional (dollar amount) for order submission
# This allows Alpaca to handle fractional shares automatically
if dollar_amount <= 0:
message = (
f"Cannot open position for {ticker}: "
f"non-positive quantity calculated from dollar_amount {dollar_amount} "
f"and price {current_price}"
f"non-positive dollar_amount {dollar_amount}"
)
raise ValueError(message)

try:
self.trading_client.submit_order(
order_data=OrderRequest(
symbol=ticker.upper(),
qty=qty,
notional=dollar_amount,
side=OrderSide(side.value.lower()),
type=OrderType.MARKET,
time_in_force=TimeInForce.DAY,
Comment thread
forstmeier marked this conversation as resolved.
Expand Down
4 changes: 1 addition & 3 deletions applications/portfoliomanager/src/portfoliomanager/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@


class PositionAction(Enum):
PDT_LOCKED = "PDT_LOCKED"
CLOSE_POSITION = "CLOSE_POSITION"
MAINTAIN_POSITION = "MAINTAIN_POSITION"
OPEN_POSITION = "OPEN_POSITION"
CLOSE_POSITION = "CLOSE_POSITION"
UNSPECIFIED = "UNSPECIFIED"

Comment thread
forstmeier marked this conversation as resolved.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,8 @@ def check_position_side_sums(
checks=[
pa.Check.isin(
[
PositionAction.PDT_LOCKED.value,
PositionAction.CLOSE_POSITION.value,
PositionAction.OPEN_POSITION.value,
PositionAction.MAINTAIN_POSITION.value,
PositionAction.CLOSE_POSITION.value,
PositionAction.UNSPECIFIED.value,
]
),
Expand Down
Loading