Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8373092
Fix inter-service communication patterns
forstmeier Jun 3, 2025
22a9c60
Update application/positionmanager/src/positionmanager/clients.py
chrisaddy Jun 3, 2025
2887ceb
Merge branch 'master' into 06-02-fix_inter-service_communication_patt…
chrisaddy Jun 3, 2025
00ffd1d
Integrate predictionengine into the workflows
forstmeier Jun 5, 2025
49b392f
Merge pull request #588 from pocketsizefund/06-04-integrate_predictio…
chrisaddy Jun 5, 2025
a1c7e64
Merge branch 'master' into 06-02-fix_inter-service_communication_patt…
chrisaddy Jun 5, 2025
b870adf
rebasing master
chrisaddy Jun 5, 2025
3b05fa4
Various fixes
forstmeier Jun 5, 2025
c329c4b
Add basic tests for datamanager
forstmeier Jun 5, 2025
3e4f77c
Merge pull request #590 from pocketsizefund/06-04-add_basic_tests_for…
chrisaddy Jun 6, 2025
8f42c14
Merge branch 'master' into 06-02-fix_inter-service_communication_patt…
forstmeier Jun 6, 2025
f872aae
Remove Flox environment variables to fix breaking tests
forstmeier Jun 6, 2025
dc37574
Merge branch 'master' into 06-02-fix_inter-service_communication_patt…
forstmeier Jun 6, 2025
14a6984
fix some linting issues
chrisaddy Jun 6, 2025
43af2da
Fix inter-service communication patterns
forstmeier Jun 3, 2025
0be3104
Integrate predictionengine into the workflows
forstmeier Jun 5, 2025
7a2cb67
fix some linting issues
chrisaddy Jun 6, 2025
3a43f98
fix linting
chrisaddy Jun 6, 2025
a75adf0
Fix inter-service communication patterns
forstmeier Jun 3, 2025
494b0a5
Integrate predictionengine into the workflows
forstmeier Jun 5, 2025
973ad6b
remove vars
chrisaddy Jun 6, 2025
19107c0
Merge pull request #591 from pocketsizefund/fix/linting
chrisaddy Jun 6, 2025
70739d6
Add linting fixes
forstmeier Jun 6, 2025
e0bbcb6
Fix CodeRabbit feedback
forstmeier Jun 6, 2025
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
5 changes: 3 additions & 2 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
"Bash(gh issue close:*)"
],
"deny": []
}
}
},
"enableAllProjectMcpServers": false
Comment thread
chrisaddy marked this conversation as resolved.
}
Comment thread
chrisaddy marked this conversation as resolved.
8 changes: 0 additions & 8 deletions .flox/env/manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ nushell.pkg-path = "nushell"
fd.pkg-path = "fd"
fselect.pkg-path = "fselect"

[vars]
ALPACA_API_KEY = "${ALPACA_API_KEY}"
ALPACA_API_SECRET = "${ALPACA_API_SECRET}"
POLYGON_API_KEY = "${POLYGON_API_KEY}"
DATA_BUCKET = "${DATA_BUCKET}"
DUCKDB_ACCESS_KEY = "${DUCKDB_ACCESS_KEY}"
DUCKDB_SECRET = "${DUCKDB_SECRET}"

[options]
systems = [
"aarch64-darwin",
Expand Down
2 changes: 1 addition & 1 deletion application/datamanager/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.12
FROM python:3.12.10

COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

Expand Down
2 changes: 1 addition & 1 deletion application/datamanager/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "datamanager"
version = "0.1.0"
description = "Data management service"
requires-python = "==3.13"
requires-python = "==3.12.10"
dependencies = [
"fastapi>=0.115.12",
"uvicorn>=0.34.2",
Expand Down
6 changes: 3 additions & 3 deletions application/datamanager/src/datamanager/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ async def fetch_equity_bars(request: Request, summary_date: SummaryDate) -> Bars
polygon = request.app.state.settings.polygon
bucket = request.app.state.settings.gcp.bucket

summary_date: str = summary_date.date.strftime("%Y-%m-%d")
url = f"{polygon.base_url}{polygon.daily_bars}{summary_date}"
request_summary_date: str = summary_date.date.strftime("%Y-%m-%d")
url = f"{polygon.base_url}{polygon.daily_bars}{request_summary_date}"
logger.info(f"polygon_api_endpoint={url}")

params = {"adjusted": "true", "apiKey": polygon.api_key}
Expand Down Expand Up @@ -178,7 +178,7 @@ async def fetch_equity_bars(request: Request, summary_date: SummaryDate) -> Bars
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to write data",
) from e
return BarsSummary(date=summary_date, count=count)
return BarsSummary(date=request_summary_date, count=count)


@application.delete("/equity-bars")
Expand Down
93 changes: 93 additions & 0 deletions application/datamanager/tests/test_datamanager_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import unittest
from datetime import date
from unittest.mock import MagicMock, patch

from fastapi import status
from fastapi.testclient import TestClient

from application.datamanager.src.datamanager.main import application
from application.datamanager.src.datamanager.models import BarsSummary, SummaryDate

client = TestClient(application)


def test_health_check() -> None:
response = client.get("/health")
assert response.status_code == status.HTTP_200_OK


class TestDataManagerModels(unittest.TestCase):
def test_summary_date_default(self) -> None:
summary_date = SummaryDate()
assert isinstance(summary_date.date, date)

def test_summary_date_with_date(self) -> None:
test_date = date(2023, 1, 1)
summary_date = SummaryDate(date=test_date)
assert summary_date.date == test_date

def test_summary_date_string_parsing(self) -> None:
summary_date = SummaryDate(date="2023-01-01") # type: ignore
assert summary_date.date == date(2023, 1, 1)

def test_bars_summary_creation(self) -> None:
bars_summary = BarsSummary(date="2023-01-01", count=100)
assert bars_summary.date == "2023-01-01"
assert bars_summary.count == 100 # noqa: PLR2004


class TestEquityBarsEndpoints(unittest.TestCase):
def test_get_equity_bars_missing_parameters(self) -> None:
response = client.get("/equity-bars")
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

def test_get_equity_bars_invalid_date_format(self) -> None:
response = client.get(
"/equity-bars",
params={"start_date": "invalid-date", "end_date": "2023-01-02"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

def test_post_equity_bars_missing_body(self) -> None:
response = client.post("/equity-bars")
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

def test_post_equity_bars_invalid_date(self) -> None:
response = client.post("/equity-bars", json={"date": "invalid-date"})
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

def test_delete_equity_bars_missing_body(self) -> None:
response = client.request("DELETE", "/equity-bars")
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

def test_delete_equity_bars_invalid_date(self) -> None:
response = client.request(
"DELETE", "/equity-bars", json={"date": "invalid-date"}
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

@patch("application.datamanager.src.datamanager.main.duckdb")
def test_get_equity_bars_database_error(self, mock_duckdb: MagicMock) -> None:
from duckdb import IOException

mock_connection = MagicMock()
mock_connection.execute.side_effect = IOException("Database error")
mock_duckdb.connect.return_value = mock_connection

mock_settings = MagicMock()
mock_settings.gcp.bucket.name = "test-bucket"

with patch.object(application, "state") as mock_app_state:
mock_app_state.connection = mock_connection
mock_app_state.settings = mock_settings

response = client.get(
"/equity-bars",
params={"start_date": "2023-01-01", "end_date": "2023-01-02"},
)

assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR


if __name__ == "__main__":
unittest.main()
131 changes: 131 additions & 0 deletions application/datamanager/tests/test_datamanager_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import unittest
from datetime import date

import pytest
from pydantic import ValidationError

from application.datamanager.src.datamanager.models import (
BarsSummary,
DateRange,
SummaryDate,
)


class TestSummaryDate(unittest.TestCase):
def test_summary_date_initialization_default(self) -> None:
summary_date = SummaryDate()
assert isinstance(summary_date.date, date)

def test_summary_date_initialization_with_date(self) -> None:
test_date = date(2023, 5, 15)
summary_date = SummaryDate(date=test_date)
assert summary_date.date == test_date

def test_summary_date_string_parsing_iso_format(self) -> None:
summary_date = SummaryDate(date="2023-5-15") # type: ignore
assert summary_date.date == date(2023, 5, 15)
Comment thread
forstmeier marked this conversation as resolved.

def test_summary_date_string_parsing_slash_format(self) -> None:
summary_date = SummaryDate(date="2023/05/15") # type: ignore
assert summary_date.date == date(2023, 5, 15)

def test_summary_date_invalid_format(self) -> None:
with pytest.raises(ValidationError, match="Invalid date format"):
SummaryDate(date="invalid-date") # type: ignore

def test_summary_date_invalid_date_values(self) -> None:
with pytest.raises(ValidationError):
SummaryDate(date="2023-13-01") # type: ignore

def test_summary_date_json_encoder(self) -> None:
test_date = date(2023, 5, 15)
summary_date = SummaryDate(date=test_date)
json_data = summary_date.model_dump(mode="json")
assert json_data["date"] == "2023/05/15"


class TestDateRange(unittest.TestCase):
def test_date_range_valid(self) -> None:
start_date = date(2023, 1, 1)
end_date = date(2023, 12, 31)
date_range = DateRange(start=start_date, end=end_date)

assert date_range.start == start_date
assert date_range.end == end_date

def test_date_range_same_dates(self) -> None:
same_date = date(2023, 5, 15)
with pytest.raises(ValidationError, match="End date must be after start date"):
DateRange(start=same_date, end=same_date)

def test_date_range_end_before_start(self) -> None:
start_date = date(2023, 12, 31)
end_date = date(2023, 1, 1)
with pytest.raises(ValidationError, match="End date must be after start date"):
DateRange(start=start_date, end=end_date)

def test_date_range_valid_one_day_apart(self) -> None:
start_date = date(2023, 5, 15)
end_date = date(2023, 5, 16)
date_range = DateRange(start=start_date, end=end_date)

assert date_range.start == start_date
assert date_range.end == end_date


class TestBarsSummary(unittest.TestCase):
def test_bars_summary_initialization(self) -> None:
bars_summary = BarsSummary(date="2023-05-15", count=1500)

assert bars_summary.date == "2023-05-15"
assert bars_summary.count == 1500 # noqa: PLR2004

def test_bars_summary_zero_count(self) -> None:
bars_summary = BarsSummary(date="2023-05-15", count=0)

assert bars_summary.date == "2023-05-15"
assert bars_summary.count == 0

def test_bars_summary_negative_count(self) -> None:
bars_summary = BarsSummary(date="2023-05-15", count=-1)

assert bars_summary.date == "2023-05-15"
assert bars_summary.count == -1

def test_bars_summary_json_serialization(self) -> None:
bars_summary = BarsSummary(date="2023-05-15", count=1500)
json_data = bars_summary.model_dump()

assert json_data == {"date": "2023-05-15", "count": 1500}

def test_bars_summary_from_dict(self) -> None:
data = {"date": "2023-05-15", "count": 1500}
bars_summary = BarsSummary.model_validate(data)

assert bars_summary.date == "2023-05-15"
assert bars_summary.count == 1500 # noqa: PLR2004


class TestModelIntegration(unittest.TestCase):
def test_summary_date_to_bars_summary(self) -> None:
summary_date = SummaryDate(date="2023-05-15") # type: ignore
bars_summary = BarsSummary(
date=summary_date.date.strftime("%Y-%m-%d"), count=100
)

assert bars_summary.date == "2023-05-15"
assert bars_summary.count == 100 # noqa: PLR2004

def test_multiple_model_validation(self) -> None:
summary_date = SummaryDate(date="2023-05-15") # type: ignore
date_range = DateRange(start=date(2023, 1, 1), end=date(2023, 12, 31))
bars_summary = BarsSummary(date="2023-05-15", count=1000)

assert summary_date.date == date(2023, 5, 15)
assert date_range.start == date(2023, 1, 1)
assert date_range.end == date(2023, 12, 31)
assert bars_summary.count == 1000 # noqa: PLR2004


if __name__ == "__main__":
unittest.main()
3 changes: 2 additions & 1 deletion application/positionmanager/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "positionmanager"
version = "0.1.0"
description = "Position management service"
requires-python = "==3.13"
requires-python = "==3.12.10"
dependencies = [
"fastapi>=0.115.12",
"uvicorn>=0.34.2",
Expand All @@ -14,6 +14,7 @@ dependencies = [
"pyportfolioopt>=1.5.6",
"ecos>=2.0.14",
"prometheus-fastapi-instrumentator>=7.1.0",
"pyarrow>=20.0.0",
]

[tool.hatch.build.targets.wheel]
Expand Down
46 changes: 29 additions & 17 deletions application/positionmanager/src/positionmanager/clients.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any

import polars as pl
import pyarrow as pa
Comment thread
forstmeier marked this conversation as resolved.
import requests
from alpaca.trading.client import TradingClient
from alpaca.trading.enums import OrderSide, TimeInForce
Expand All @@ -18,8 +19,8 @@ def __init__(
paper: bool = True,
) -> None:
if not api_key or not api_secret:
msg = "Alpaca API key and secret are required"
raise ValueError(msg)
message = "Alpaca API key and secret are required"
raise ValueError(message)

self.trading_client: TradingClient = TradingClient(
api_key, api_secret, paper=paper
Expand All @@ -30,8 +31,8 @@ def get_cash_balance(self) -> Money:
cash_balance = getattr(account, "cash", None)

if cash_balance is None:
msg = "Cash balance is not available"
raise ValueError(msg)
message = "Cash balance is not available"
raise ValueError(message)

return Money.from_float(float(cash_balance))

Expand Down Expand Up @@ -72,32 +73,43 @@ def get_data(
date_range: DateRange,
) -> pl.DataFrame:
if not self.datamanager_base_url:
msg = "Data manager URL is not configured"
raise ValueError(msg)
message = "Data manager URL is not configured"
raise ValueError(message)

endpoint = f"{self.datamanager_base_url}/equity-bars"

params = {
"start_date": date_range.start.date().isoformat(),
"end_date": date_range.end.date().isoformat(),
}

try:
response = requests.post(endpoint, json=date_range.to_payload(), timeout=10)
response = requests.get(endpoint, params=params, timeout=30)
except requests.RequestException as err:
msg = f"Data manager service call error: {err}"
raise RuntimeError(msg) from err
message = f"Data manager service call error: {err}"
raise RuntimeError(message) from err

response.raise_for_status()
if response.status_code == requests.codes["no_content"]:
return pl.DataFrame()
if response.status_code != requests.codes["ok"]:
message = f"Data service error: {response.text}, status code: {response.status_code}" # noqa: E501
raise requests.HTTPError(
message,
response=response,
)

response_data = response.json()
buffer = pa.py_buffer(response.content)
reader = pa.ipc.RecordBatchStreamReader(buffer)
table = reader.read_all()

data = pl.DataFrame(response_data["data"])
data = pl.DataFrame(pl.from_arrow(table))
Comment thread
forstmeier marked this conversation as resolved.

data = data.with_columns(
pl.col("timestamp")
.str.slice(0, 10)
.str.strptime(pl.Date, "%Y-%m-%d")
.alias("date"),
pl.col("datetime").cast(pl.Datetime).dt.date().alias("date")
)

return (
data.sort("date")
.pivot(on="ticker", index="date", values="close_price")
.pivot(on="T", index="date", values="c")
.with_columns(pl.all().exclude("date").cast(pl.Float64))
)
2 changes: 1 addition & 1 deletion application/positionmanager/src/positionmanager/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import polars as pl
import requests
from alpaca.common.rest import APIError
from alpaca.common.exceptions import APIError
from fastapi import FastAPI, HTTPException
from prometheus_fastapi_instrumentator import Instrumentator
from pydantic import ValidationError
Expand Down
Loading