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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
.venv/
__pycache__/
.envrc
.env.nu
coverage/
.coverage
*.csv
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,46 @@ def open_position(
def close_position(
self,
ticker: str,
) -> None:
self.trading_client.close_position(
symbol_or_asset_id=ticker.upper(),
close_options=ClosePositionRequest(
percentage="100",
),
)
) -> bool:
"""Close a position for the given ticker.

time.sleep(self.rate_limit_sleep)
Returns True if position was closed, False if position didn't exist.
"""
try:
self.trading_client.close_position(
symbol_or_asset_id=ticker.upper(),
close_options=ClosePositionRequest(
percentage="100",
),
)
time.sleep(self.rate_limit_sleep)
except APIError as e:
# Prefer structured information from the Alpaca API when available,
# and fall back to matching documented error message fragments for
# backwards compatibility.
status_code = getattr(e, "status_code", None)
error_code = getattr(e, "code", None)
error_message = getattr(e, "message", None)
error_str = (
str(error_message) if error_message is not None else str(e)
).lower()

# Known Alpaca behaviours when closing a non-existent position:
# - HTTP 404 Not Found
# - Specific error_code values (e.g. "position_not_found")
# - Error messages containing "position not found"
http_not_found = 404
position_not_found = (
status_code == http_not_found
or error_code in {"position_not_found"}
or "position not found" in error_str
or "position does not exist" in error_str
)
if position_not_found:
logger.info(
"Position already closed or does not exist",
ticker=ticker,
)
return False
raise
return True
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ def add_portfolio_performance_columns(
prior_predictions = prior_predictions.clone()
prior_equity_bars = prior_equity_bars.clone()

# Ensure timestamp columns have matching types for joins and comparisons.
# Timestamps may arrive as i64 (from JSON integer serialization) or f64 (from
# Python float conversion). Unconditional casting to Float64 is simpler and
# more robust than checking dtypes, and the performance cost is negligible.
prior_portfolio = prior_portfolio.with_columns(pl.col("timestamp").cast(pl.Float64))
prior_predictions = prior_predictions.with_columns(
pl.col("timestamp").cast(pl.Float64)
)
prior_equity_bars = prior_equity_bars.with_columns(
pl.col("timestamp").cast(pl.Float64)
)

prior_portfolio_predictions = prior_portfolio.join(
other=prior_predictions,
on=["ticker", "timestamp"],
Expand Down
32 changes: 23 additions & 9 deletions applications/portfoliomanager/src/portfoliomanager/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,17 +173,31 @@ async def create_portfolio() -> Response: # noqa: PLR0911, PLR0912, PLR0915, C9
close_results = []
for close_position in close_positions:
try:
alpaca_client.close_position(
was_closed = alpaca_client.close_position(
ticker=close_position["ticker"],
)
logger.info("Closed position", ticker=close_position["ticker"])
close_results.append(
{
"ticker": close_position["ticker"],
"action": "close",
"status": "success",
}
)
if was_closed:
logger.info("Closed position", ticker=close_position["ticker"])
close_results.append(
{
"ticker": close_position["ticker"],
"action": "close",
"status": "success",
}
)
else:
logger.info(
"Position already closed or did not exist",
ticker=close_position["ticker"],
)
close_results.append(
{
"ticker": close_position["ticker"],
"action": "close",
"status": "skipped",
"reason": "position_not_found",
}
)
except Exception as e:
logger.exception(
"Failed to close position",
Expand Down
3 changes: 3 additions & 0 deletions tools/sync_equity_categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ def extract_categories(tickers: list[dict]) -> pl.DataFrame:
rows = []
for ticker_data in tickers:
ticker = ticker_data.get("ticker", "")
# Skip entries with empty or missing ticker values
if not ticker:
continue
# Filter for Common Stock and all ADR types
if ticker_data.get("type") not in EQUITY_TYPES:
continue
Expand Down
Loading