Skip to content

Restructure Python application and library resources#611

Closed
forstmeier wants to merge 1 commit intomasterfrom
07-28-restructure_python_application_and_library_resources
Closed

Restructure Python application and library resources#611
forstmeier wants to merge 1 commit intomasterfrom
07-28-restructure_python_application_and_library_resources

Conversation

@forstmeier
Copy link
Copy Markdown
Collaborator

@forstmeier forstmeier commented Jul 29, 2025

Overview

Changes

  • create internal Python library
  • update all pyproject.toml configurations for "applications" and "libraries"
  • fix failing unit tests
  • create various helper functions (e.g. create_cloud_event_*)
  • add new models "application" resources for initial model training workflow script
  • deprecating GCP resources
  • rename application to applications (for my own sanity)

Comments

This is a large block of updates but necessary to improve the uv configurations. Additionally, there's a new models package that has stuff for TFT in it (still not fully training correctly but I'll debug that and have it working soon). I also need to do a manual test/refactor of the services in applications independent of infrastructure being up so be forewarned. The Flyte stuff was complaining about the values being passed between the tasks for some reason too.

Summary by CodeRabbit

  • New Features

    • Datamanager: added Alpaca-backed calendar endpoint.
    • Models: added scripts to fetch Alpaca calendars/equity bars and a TFT training workflow.
    • Shared library: added dataset prep, TFT model, loss functions, attention/LSTM modules, CloudEvent helpers, date and equity bar models.
  • Refactor

    • Restructured workspace to applications/* and libraries/python.
    • Renamed infrastructure workflows and environment variable to ALPACA_API_KEY_ID.
    • Production run now publishes port 8080.
  • Chores

    • Expanded ignore rules.
  • Removed

    • Deprecated prediction and position manager services, CLI, Docker/Compose, and legacy workflows.
  • Tests

    • Added tests for shared library components.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jul 29, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Major refactor migrating services from application/* to applications/* and introducing a shared internal library (libraries/python). Large removal of predictionengine, positionmanager, and legacy datamanager code/tests, with new datasets/models/TFT implementations and scripts under libraries/python and applications/models. CI workflows renamed, infrastructure env/image paths updated, and workspace/tooling consolidated.

Changes

Cohort / File(s) Summary of Changes
Workspace & Tooling
.gitignore, .claude/settings.local.json, .mise.toml, Dockerfile.tests, README.md, pyproject.toml
Expanded ignores; broadened pytest permission; updated task paths/port publish; switched build context to applications/*; removed two README bullets; consolidated uv workspace, tests, lint/type-check configs.
GitHub Workflows
.github/workflows/launch_infrastructure.yaml, .github/workflows/teardown_infrastructure.yaml
Renamed workflow/job identifiers from “application” to “infrastructure”; removed an if condition in teardown; schedules/steps unchanged.
Infrastructure
infrastructure/environment_variables.py, infrastructure/images.py
Renamed ALPACA_API_KEY to ALPACA_API_KEY_ID; changed service_directory from ../application to ../applications.
Libraries: Internal Package
libraries/python/pyproject.toml, libraries/python/src/internal/*, libraries/python/tests/*
Added internal library (dataset, dates, equity_bar, loss_functions, lstm_network, mhsa_network, summaries, tft_model, variable_selection_network) and corresponding tests. Introduced quantile loss, LSTM, MHSA, TFT model, dataset, and Pydantic models.
Applications: Datamanager (new path)
applications/datamanager/pyproject.toml, applications/datamanager/src/datamanager/main.py
New pyproject; updated FastAPI module to use Alpaca client, new Date type, CloudEvent helpers; added /calendar endpoint; adjusted fetch/delete signatures and error handling.
Applications: Models
applications/models/pyproject.toml, applications/models/src/models/*
New project and three scripts: fetch Alpaca calendar, fetch equity bars, and a TFT training workflow using internal dataset/model and WandB.
Applications: Portfoliomanager
applications/portfoliomanager/pyproject.toml
New project manifest referencing internal workspace.
Removals: Predictionengine
application/predictionengine/src/predictionengine/*, application/predictionengine/tests/*, application/predictionengine/Dockerfile, application/predictionengine/compose.yaml, application/predictionengine/pyproject.toml
Deleted model, dataset, attention, loss, embedding, main API, tests, Dockerfile, compose, and packaging.
Removals: Datamanager (legacy path)
application/datamanager/*
Deleted Dockerfiles, compose, Behave features/steps, tasks, pyproject, clients.py, main.py, and tests.
Removals: Positionmanager
application/positionmanager/*
Deleted Dockerfile, pyproject, clients.py, main.py, tests; emptied init.py.
CLI & Workflows Cleanup
cli/datamanager.py, cli/pyproject.toml, workflows/*
Removed CLI (SigV4 HTTP tool) and workflow modules/pyproject (fetch_data, training workflow).

Sequence Diagram(s)

sequenceDiagram
  participant C as Client
  participant D as Datamanager API
  participant A as Alpaca API
  C->>D: GET /calendar?start&end
  D->>A: get_calendar(start, end)
  A-->>D: Calendar data
  D-->>C: 200 JSON or 404/500
Loading
sequenceDiagram
  participant O as Orchestrator
  participant M as Models Scripts
  participant L as Internal Lib
  O->>M: train_tft_model()
  M->>M: read_local_data()
  M->>L: build dataset/model (Parameters, TFT)
  M->>M: train_model()/validate_model()
  M->>M: save_model()
  M-->>O: workflow complete
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

Suggested labels

infrastructure

Suggested reviewers

  • chrisaddy

Poem

A rabbit hops through folders wide,
From application to applications’ side.
Old engines fade, new libraries bloom,
TFTs train in a tidy room.
Calendars fetched, bars in a row—
Carrots of code where workflows flow.
Thump-thump! Onward we go. 🥕✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 07-28-restructure_python_application_and_library_resources

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@forstmeier forstmeier self-assigned this Jul 29, 2025
@forstmeier forstmeier added this to the Refactor milestone Jul 29, 2025
@forstmeier forstmeier force-pushed the 07-28-restructure_python_application_and_library_resources branch 2 times, most recently from 8765605 to 8460289 Compare July 29, 2025 03:31
@forstmeier forstmeier force-pushed the 07-28-restructure_python_application_and_library_resources branch 7 times, most recently from e675d1a to 17c9a34 Compare August 14, 2025 03:18
@forstmeier forstmeier marked this pull request as ready for review August 14, 2025 03:27
Copilot AI review requested due to automatic review settings August 14, 2025 03:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR restructures the Python application architecture by creating an internal library for shared resources and reorganizing the project structure. It updates configuration files to properly support both applications and libraries under the uv package manager, while consolidating common functionality into a centralized library.

  • Creates an internal Python library to consolidate shared resources across applications
  • Restructures project layout from application/ to applications/ with proper uv workspace configuration
  • Adds a new models application for machine learning workflows with TFT model training capabilities

Reviewed Changes

Copilot reviewed 62 out of 81 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pyproject.toml Updates workspace configuration to support applications and libraries structure
libraries/python/ Creates new internal library with shared models, utilities, and ML components
applications/ Restructures and updates all service applications to use the internal library
workflows/ Removes standalone workflow package in favor of models application
infrastructure/ Updates path references to match new applications directory structure

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread applications/models/pyproject.toml Outdated
Comment thread applications/datamanager/src/datamanager/main.py Outdated
Comment thread libraries/python/src/internal/loss_functions.py Outdated
Comment thread libraries/python/src/internal/lstm_network.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 47

🔭 Outside diff range comments (8)
applications/positionmanager/src/positionmanager/portfolio.py (3)

11-16: Min/max tickers ctor parameters are unused

The initializer captures minimum_portfolio_tickers and maximum_portfolio_tickers, but they aren’t enforced in get_optimized_portfolio. Either implement enforcement or remove the parameters to avoid confusion.

Options:

  • Enforce max by zeroing smallest weights until non-zero count ≤ maximum_portfolio_tickers.
  • Enforce min by boosting weights of top-N assets or adding a post-optimization selection heuristic.

I can provide a small post-processing helper that trims weights to top-N by absolute weight and re-normalizes.


22-23: Validate prediction_weight ∈ [0, 1]

Guard against invalid values to avoid unintentionally flipping the sign or overweighting predictions.

Suggested change:

     prediction_weight: float = 0.3,
 ) -> dict[str, int]:
-        converted_data = historical_data.to_pandas()
+        if not 0.0 <= prediction_weight <= 1.0:
+            raise ValueError("prediction_weight must be between 0 and 1 inclusive")
+        converted_data = historical_data.to_pandas()

54-55: Avoid truncating portfolio value to an int

Casting Money to float and then to int truncates cents, systematically under-allocating capital. DiscreteAllocation accepts floats; preserve precision.

-            total_portfolio_value=int(float(portfolio_value)),
+            total_portfolio_value=float(portfolio_value),
pyproject.toml (1)

17-28: Pytest rootdir points to a non-existent path; likely breaks test discovery

Setting --rootdir=/tests while tests live under applications/*/tests and libraries/python/tests can cause test discovery issues.

Consider removing rootdir or pointing it to the repo root.

 addopts = [
     "--verbose",
     "--tb=short",
     "--strict-markers",
     "--color=yes",
-    "--rootdir=/tests",
 ]
libraries/python/src/internal/dates.py (1)

38-45: Type annotations in validator should use datetime.date, not datetime.datetime

start and end are datetime.date, but the validator type hints are datetime.datetime. This is misleading and could confuse IDEs/type-checkers.

Apply this diff:

-    def check_end_after_start(
-        cls,
-        end_value: datetime.datetime,
-        info: core_schema.ValidationInfo,
-    ) -> datetime.datetime:
+    def check_end_after_start(
+        cls,
+        end_value: datetime.date,
+        info: core_schema.ValidationInfo,
+    ) -> datetime.date:
applications/datamanager/src/datamanager/main.py (1)

179-191: Content-Type mismatch: writing Arrow stream but declaring file; fix media_type and Content-Disposition.

You’re using pa.ipc.RecordBatchStreamWriter (stream format) but returning media_type="application/vnd.apache.arrow.file". Use the stream media type and standard Content-Disposition formatting.

Apply this diff:

-            content_disposition = f"attachment; {filename=}"
+            content_disposition = f'attachment; filename="{filename}"'
...
-        return Response(
-            content=sink.getvalue().to_pybytes(),
-            media_type="application/vnd.apache.arrow.file",
+        return Response(
+            content=sink.getvalue().to_pybytes(),
+            media_type="application/vnd.apache.arrow.stream",
             headers={
                 "Content-Disposition": content_disposition,
                 "X-Row-Count": str(data.num_rows),
                 "X-Start-Date": str(start_date),
                 "X-End-Date": str(end_date),
             },
         )
applications/positionmanager/tests/test_positionmanager_main.py (1)

88-106: Handle HTTPException in /positions/open so errors return CloudEvent

Verified: open_position returns CloudEvent for several exceptions but does NOT catch fastapi.HTTPException. The tests set AlpacaClient.get_cash_balance to raise HTTPException, which currently bubbles up (resulting in a 500) — that's why the test expects a 500. To make error handling consistent, convert HTTPException into a CloudEvent error.

Actionable changes:

  • applications/positionmanager/src/positionmanager/main.py

    • In open_position (around @application.post("/positions/open")), include HTTPException in the except clause that handles get_cash_balance (or add a dedicated except HTTPException to return create_cloud_event_error). Example:
      except (HTTPException, requests.RequestException, APIError, ValidationError) as e:
      return create_cloud_event_error(...)
  • applications/positionmanager/tests/test_positionmanager_main.py

    • Update test_open_position_alpaca_error to assert the standardized CloudEvent error response (and status code consistent with your contract) instead of asserting a raw 500 and HTTPException detail.

Reasoning (short): current behavior is intentional given the code, but inconsistent with the rest of the endpoint's CloudEvent error returns — adding HTTPException handling (or a global handler) will align runtime behavior and tests.

applications/positionmanager/src/positionmanager/main.py (1)

175-183: Guard against missing percentile_50 in predictions to avoid KeyError.

Use .get and filter only valid entries.

Apply this diff:

-        predictions_percentile_50 = {
-            key: value["percentile_50"] for key, value in predictions.items()
-        }
+        predictions_percentile_50 = {
+            key: value.get("percentile_50")
+            for key, value in predictions.items()
+            if isinstance(value, dict) and "percentile_50" in value
+        }
♻️ Duplicate comments (4)
libraries/python/src/internal/loss_functions.py (1)

20-26: Bug: add() result is unused; loss accumulation is a no-op without assignment

In tinygrad most ops return new tensors. errors_total.add(...) does not mutate in place; you need to assign back. This matches a prior review comment.

-        errors_total.add(
+        errors_total = errors_total.add(
             Tensor.where(
                 error > 0,
                 quantile_tensor.mul(error),
                 (quantile_tensor.sub(1)).mul(error),
             ).mean()
         )
applications/models/pyproject.toml (1)

7-19: Remove duplicate 'internal==0.1.0' dependency

The 'internal' dependency is listed twice (Lines 7 and 13). Keep a single entry to avoid resolver confusion.

Apply this diff:

 dependencies = [
-    "internal==0.1.0",
+    "internal==0.1.0",
@@
-    "polygon-api-client>=1.14.6",
-    "internal==0.1.0",
+    "polygon-api-client>=1.14.6",

Additionally, since you’re using a UV workspace source below, you can depend on the workspace package without pinning a version to avoid conflicts between the lock and local workspace:

```diff
-[tool.uv.sources]
-internal = { workspace = true }
+[tool.uv.sources]
+internal = { workspace = true }

And update the dependency to reference the workspace:

-    "internal==0.1.0",
+    "internal",

If you prefer to keep the exact version pin for releases, do it in non-workspace builds only (e.g., via an env-driven dependency override).

libraries/python/src/internal/lstm_network.py (1)

80-80: Tensor.stack likely fails for a single-timestep sequence

Tinygrad’s Tensor.stack generally expects multiple tensors. The previous code handled the single-output case via unsqueeze. Restore that branch to prevent runtime errors when sequence_length == 1.

Apply this diff:

-        output_tensor = Tensor.stack(outputs[0], *outputs[1:], dim=1)
+        if len(outputs) == 1:
+            output_tensor = outputs[0].unsqueeze(dim=1)
+        else:
+            output_tensor = Tensor.stack(outputs[0], *outputs[1:], dim=1)
applications/datamanager/src/datamanager/main.py (1)

265-281: Broad exception mapped to 404; return 500 on listing error and clarify log message.

An exception when listing S3 objects is not “not found”; it’s an internal error. Also, error message should reflect the actual operation.

Apply this diff:

-    try:
-        blobs = list(s3_client.list_objects(prefix=prefix))
-    except Exception as e:  # noqa: BLE001
-        logger.error(f"Error listing S3 objects: {e}")
-        return Response(
-            status_code=status.HTTP_404_NOT_FOUND,
-            content=json.dumps({"error": "No equity bars found"}),
-            media_type="application/json",
-        )
+    try:
+        blobs = list(s3_client.list_objects(prefix=prefix))
+    except Exception as e:  # noqa: BLE001
+        logger.error(f"Error listing equity bars: {e}")
+        return Response(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            content=json.dumps({"error": "Failed to list equity bars"}),
+            media_type="application/json",
+        )
🧹 Nitpick comments (47)
applications/positionmanager/src/positionmanager/portfolio.py (3)

26-31: Indexing date column: ensure proper dtype

Setting the index to "date" is correct; consider ensuring a datetime index for robustness with time-based operations.

Optional:

-        if "date" in converted_data.columns:
-            converted_data = converted_data.set_index("date")
+        if "date" in converted_data.columns:
+            converted_data["date"] = pd.to_datetime(converted_data["date"], errors="coerce")
+            converted_data = converted_data.set_index("date").dropna(axis=0, how="any")

46-47: Make risk-free rate configurable

Hardcoding 2% limits reuse across environments. Consider an argument or class attribute.

-        efficient_frontier.max_sharpe(risk_free_rate=0.02)  # 2% risk-free rate
+        efficient_frontier.max_sharpe(risk_free_rate=getattr(self, "risk_free_rate", 0.02))

57-59: Consider lp_portfolio for better discrete allocation

greedy_portfolio is fast but suboptimal. If runtime allows (and solver deps are present), lp_portfolio can yield better allocations and help adhere to constraints (e.g., bounds).

Example:

optimized_portfolio_ticker_share_counts, _ = discrete_allocation.lp_portfolio()

Keep greedy as a fallback if solver unavailable.

applications/positionmanager/pyproject.toml (1)

5-5: Exact Python pin may be too strict for deployability

Pinning requires-python to an exact micro version (==3.12.10) can block environments that provide 3.12.11+ security releases. Consider a compatible range:

-requires-python = "==3.12.10"
+requires-python = ">=3.12,<3.13"

If your infra tooling enforces 3.12.10 specifically (e.g., via container tags), feel free to keep it exact.

libraries/python/pyproject.toml (1)

1-4: Optional: add a build backend for non-uv environments

If anyone installs/builds this package outside uv (e.g., plain pip, IDEs, CI builders lacking uv), consider adding a PEP 517 build backend. Example with hatchling:

+[build-system]
+requires = ["hatchling>=1.25.0"]
+build-backend = "hatchling.build"

This won’t affect uv but improves portability.

applications/predictionengine/pyproject.toml (1)

5-5: Avoid exact patch pin for Python; use a compatible range

Exact patch pin in requires-python is brittle and may block installs in equivalent 3.12.x environments (e.g., base images differing by patch). A typical pattern is “>=3.12,<3.13”.

-requires-python = "==3.12.10"
+requires-python = ">=3.12,<3.13"
infrastructure/images.py (1)

15-17: Also validate Dockerfile presence to fail fast with actionable error

Directory existence is necessary but not sufficient. Checking for a Dockerfile avoids later opaque build errors.

     if not service_directory.exists():
         message = f"Service directory not found: {service_directory}"
         raise FileNotFoundError(message)
+
+    dockerfile = service_directory / "Dockerfile"
+    if not dockerfile.exists():
+        raise FileNotFoundError(f"Dockerfile not found: {dockerfile}")
.github/workflows/teardown_infrastructure.yaml (1)

5-5: Clarify time zone in the comment for cron

GitHub cron uses UTC. The comment currently says “EST” which is misleading during daylight savings.

-    - cron: '0 23 * * 1,2,3,4,5' # teardown at 6:00 PM EST
+    - cron: '0 23 * * 1,2,3,4,5' # 23:00 UTC (6pm ET standard time, 7pm ET daylight time)
.github/workflows/launch_infrastructure.yaml (2)

2-8: Clarify cron timezone in comment to avoid confusion

GitHub Actions cron uses UTC. The comment “8:00 AM EST” will be incorrect during daylight saving time (it’ll run 9:00 AM ET). Suggest updating the comment to explicitly note UTC.

-    - cron: '0 13 * * 1,2,3,4,5'  # launch at 8:00 AM EST
+    - cron: '0 13 * * 1,2,3,4,5'  # 13:00 UTC (8 AM ET standard time / 9 AM ET daylight time)

7-15: Avoid overlapping scheduled runs; add job-level concurrency and minimal permissions

Prevent concurrent “launch” jobs from stepping on each other (e.g., if a previous run overruns) and minimize token scope.

 jobs:
   launch_infrastructure:
+    permissions:
+      contents: read
     name: Launch infrastructure on weekday schedule
     runs-on: ubuntu-latest
     environment: pulumi
+    concurrency:
+      group: launch_infrastructure-${{ github.ref }}
+      cancel-in-progress: true
pyproject.toml (1)

5-5: Exact Python version pin reduces portability

requires-python ==3.12.10 is extremely strict. Unless you require that exact patch version, consider a compatible range (e.g., >=3.12,<3.13) to ease local/CI/environment differences.

-requires-python = "==3.12.10"
+requires-python = ">=3.12,<3.13"
libraries/python/src/internal/loss_functions.py (1)

16-28: Optional: Vectorize across quantiles to reduce Python overhead

You can compute the loss for all quantiles in one pass by broadcasting targets and quantiles; this is simpler and faster.

-    errors_total = Tensor.zeros((1,))
-    for index, quantile in enumerate(quantiles):
-        error = targets.sub(predictions[:, :, index])
-        quantile_tensor = Tensor([quantile])
-        errors_total = errors_total.add(
-            Tensor.where(
-                error > 0,
-                quantile_tensor.mul(error),
-                (quantile_tensor.sub(1)).mul(error),
-            ).mean()
-        )
-
-    return errors_total.div(len(quantiles))  # shape: (1,)
+    # Broadcast targets to (B, O, K) and quantiles to (1, 1, K)
+    errors = targets[:, :, None].sub(predictions)
+    q = Tensor(quantiles)[None, None, :]
+    losses = Tensor.where(errors > 0, q.mul(errors), (q.sub(1)).mul(errors))
+    # Mean over batch and output dims, then average over quantiles
+    return losses.mean(axis=(0, 1)).mean(keepdims=True)  # shape: (1,)
libraries/python/tests/test_loss_functions.py (1)

32-39: Strengthen assertion for perfect prediction

For zero error, quantile loss should be exactly 0 within numerical tolerance. Tighten the check to assert near-zero.

-    assert loss.numpy() >= 0.0
+    assert abs(float(loss.numpy())) < 1e-6
applications/positionmanager/src/positionmanager/clients.py (3)

120-123: Prefer using DateRange.to_object() for consistency and encapsulation

You already expose a helper to serialize DateRange; use it to avoid duplication and future drift.

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

126-139: HTTP ergonomics: use raise_for_status and explicit Accept header (optional)

  • You can lean on response.raise_for_status() for non-204 cases to simplify error handling.
  • If the service returns Arrow stream, advertise it explicitly to guard against content-type drift.
-        try:
-            response = requests.get(endpoint, params=params, timeout=30)
+        headers = {"Accept": "application/vnd.apache.arrow.stream"}
+        try:
+            response = requests.get(endpoint, params=params, headers=headers, timeout=(5, 30))
         except requests.RequestException as err:
             message = f"Data manager service call error: {err}"
             raise RuntimeError(message) from err
 
         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.raise_for_status()

144-154: Minor: avoid redundant DataFrame wrapping and favor simpler conversion

pl.from_arrow already returns a Polars DataFrame; no need to re-wrap.

-        data = pl.DataFrame(pl.from_arrow(table))
+        data = pl.from_arrow(table)
 
         data = data.with_columns(
             pl.col("datetime").cast(pl.Datetime).dt.date().alias("date")
         )
libraries/python/src/internal/cloud_event.py (1)

12-21: Optional: accept timezone as a parameter for consistency in non-ET contexts

If these helpers are used outside ET, hardcoding America/New_York can be surprising. Consider adding an optional tz parameter defaulting to ET.

I can draft a small refactor to thread an optional tz: ZoneInfo | None parameter if you want to make this configurable.

Also applies to: 29-38

applications/models/pyproject.toml (2)

5-5: Consider broadening requires-python to allow patch updates

Pinning to an exact patch version (==3.12.10) can cause churn and friction in CI images. Unless there’s a known incompatibility, prefer a compatible range like >=3.12,<3.13.

Apply this diff:

-requires-python = "==3.12.10"
+requires-python = ">=3.12,<3.13"

8-9: Avoid explicitly depending on botocore alongside boto3

boto3 manages its botocore pin. Declaring both can create resolver conflicts. Remove botocore unless you truly use it directly.

Apply this diff:

-    "botocore>=1.38.23",
applications/models/src/models/get_alpaca_equity_bars.py (1)

55-72: Defensive parsing: optional fields may be missing in bars

vw can be missing; using direct indexing can raise KeyError. Prefer .get with default None.

Apply this diff:

-                "volume_weighted_average_price": bar["vw"],
+                "volume_weighted_average_price": bar.get("vw"),
libraries/python/src/internal/lstm_network.py (1)

24-30: Avoid shadowing the input_size parameter inside the layer loop

Shadowing makes the code harder to read. Use a local name for the per-layer input size.

Apply this diff:

-        for index in range(layer_count):
-            input_size = input_size if index == 0 else self.hidden_size
+        for index in range(layer_count):
+            layer_input_size = input_size if index == 0 else self.hidden_size
             self.layers.append(
                 LSTMCell(
-                    input_size=input_size,
+                    input_size=layer_input_size,
                     hidden_size=self.hidden_size,
                 )
             )
libraries/python/tests/test_lstm_network.py (1)

12-17: Verify the minimum layer_count enforcement in LSTM class.

The test correctly initializes LSTM with layer_count=3, which aligns with the internal implementation that enforces a minimum of 3 layers. The test should also verify that attempting to initialize with fewer layers raises a ValueError.

Would you like me to generate an additional test case to verify that the LSTM class properly enforces the minimum layer count requirement?

libraries/python/src/internal/dataset.py (2)

10-26: Add type hints for Scaler attributes.

The Scaler class is missing type hints for its attributes means and standard_deviations. Consider adding them for better type safety.

 class Scaler:
     def __init__(self) -> None:
-        pass
+        self.means: pl.DataFrame | None = None
+        self.standard_deviations: pl.DataFrame | None = None

116-117: Avoid using boolean literals with noqa comments.

The code uses # noqa: FBT003 to suppress linting warnings about boolean literals. Consider using variables for better readability.

+        holiday_true = True
+        holiday_false = False
         data = (
             data.with_columns(
                 pl.col("datetime").dt.weekday().alias("temporary_weekday")
             )
             .with_columns(
                 pl.when(
                     pl.col("is_holiday").is_null()
                     & (pl.col("temporary_weekday") <= friday_number)
                 )
-                .then(True)  # noqa: FBT003
+                .then(holiday_true)
                 .when(
                     pl.col("is_holiday").is_null()
                     & (pl.col("temporary_weekday") > friday_number)
                 )
-                .then(False)  # noqa: FBT003
+                .then(holiday_false)

Also applies to: 121-122

libraries/python/tests/test_dataset.py (1)

81-134: Add validation for missing "targets" key in predict mode.

The test correctly validates batch structure for prediction mode. Consider also testing that the "targets" key is NOT present in predict mode batches, as it should only be included for train/validate modes.

     for batch in batches:
         assert "encoder_categorical_features" in batch
         assert "encoder_continuous_features" in batch
         assert "decoder_categorical_features" in batch
         assert "static_categorical_features" in batch
+        # Verify targets are not included in predict mode
+        assert "targets" not in batch
Dockerfile.tests (1)

30-30: Potentially unnecessary early copy of sources

Unless uv needs the full library source to resolve workspace/local path deps during sync, keep full libraries/ copy after uv sync to avoid cache invalidation on code-only changes.

libraries/python/tests/test_mhsa_network.py (3)

9-12: Nit: reuse variables instead of hardcoded literals

Prefer passing heads_count and embedding_size variables to avoid divergence if they change.

-    attention = MultiHeadSelfAttentionNetwork(heads_count=8, embedding_size=64)
+    attention = MultiHeadSelfAttentionNetwork(
+        heads_count=heads_count, embedding_size=embedding_size
+    )

31-36: Add a negative test for invalid configuration (embedding not divisible by heads)

The network should reject incompatible configurations (e.g., embedding_size not divisible by heads_count). Add a test to assert a ValueError (or the specific exception you raise).

Example test to add:

def test_multi_head_attention_invalid_heads_divisibility() -> None:
    # embedding_size=30 is not divisible by heads_count=8
    with pytest.raises(ValueError):
        _ = MultiHeadSelfAttentionNetwork(heads_count=8, embedding_size=30)

17-22: Optional: seed RNG for determinism (if future assertions go beyond shapes)

Currently you only assert shapes, so nondeterminism is fine. If you later assert on values, seed both numpy and tinygrad RNGs to prevent flakes.

libraries/python/src/internal/dates.py (1)

8-31: Consider Pydantic v2 serializers instead of json_encoders

In Pydantic v2, field_serializer is the preferred way to control JSON serialization. json_encoders may continue to work, but moving to serializers is more idiomatic and explicit.

Example:

 class Date(BaseModel):
@@
-    model_config = {"json_encoders": {datetime.date: lambda d: d.strftime("%Y/%m/%d")}}
+    @pydantic.field_serializer("date", when_used="json")
+    def serialize_date(self, value: datetime.date) -> str:
+        return value.strftime("%Y/%m/%d")

Note: add from pydantic import field_serializer import or reference via pydantic.field_serializer.

libraries/python/src/internal/variable_selection_network.py (1)

1-22: Confirm parameter registration and consider normalization

Short summary: tft_model assigns the two VSNs to self and calls get_parameters(self) — please verify that tinygrad.nn.state.get_parameters actually traverses object attributes and includes parameters from nested nn.Linear instances. I couldn't import tinygrad here to inspect get_parameters, so this is inconclusive from the sandbox.

Points to check:

  • libraries/python/src/internal/tft_model.py
    • self.encoder_variable_selection_network = VariableSelectionNetwork(...)
    • self.decoder_variable_selection_network = VariableSelectionNetwork(...)
    • self.parameters = get_parameters(self)
      => Ensure get_parameters(self) includes parameters from those VSN attributes.
  • libraries/python/src/internal/variable_selection_network.py
    • VariableSelectionNetwork.forward currently returns inputs.sigmoid() — if you want a TFT-style attention/gating across features, consider normalizing across the feature dimension (softmax) instead of per-feature sigmoid.

Suggested diff (optional — only if tinygrad.Tensor.softmax supports dim):

     def forward(self, inputs: Tensor) -> Tensor:
         inputs = self.input_layer(inputs)
         inputs = inputs.relu()
         inputs = self.output_layer(inputs)
-        return inputs.sigmoid()
+        # Normalize across the feature dimension for simplex-style weights
+        return inputs.softmax(dim=-1)  # verify tinygrad's softmax signature

Quick local verification you can run:

  • Inspect get_parameters source in your environment:
    python - <<'PY'
    import inspect, tinygrad.nn.state as s
    print(inspect.getsource(s.get_parameters))
    PY
  • Or check whether get_parameters(self) actually contains parameters from self.encoder_variable_selection_network and self.decoder_variable_selection_network at runtime.

If get_parameters does not collect nested attributes, explicitly aggregate VSN parameters (e.g., extend the parameter list with get_parameters(self.encoder_variable_selection_network) and get_parameters(self.decoder_variable_selection_network)).

applications/datamanager/tests/test_datamanager_main.py (2)

21-33: Replacements from SummaryDate to Date are correct; add a JSON serialization test to preserve behavior

The basics are covered. Add a test to ensure the JSON encoder for Date still outputs YYYY/MM/DD as previously asserted for SummaryDate.

Example:

def test_date_json_encoder() -> None:
    d = Date(date=date(2023, 5, 15))
    json_data = d.model_dump(mode="json")
    assert json_data["date"] == "2023/05/15"

156-175: S3Client patching OK; consider adding a test for empty key listing vs missing path semantics

You handle the "not found" case where list_objects returns empty. Consider also testing when data_bucket_name or the path prefix is misconfigured to ensure the endpoint fails fast with a clear error.

libraries/python/tests/test_equity_bar.py (2)

133-146: Align test name with behavior: pass a string to exercise the timestamp parser

The test name says “string iso format” but the test passes a date object. Pass a string to cover the validator’s string parsing path.

Apply this diff:

-    equity_bar = EquityBar(
-        ticker="AAPL",
-        timestamp=date.fromisoformat("2023-06-15"),
+    equity_bar = EquityBar(
+        ticker="AAPL",
+        timestamp="2023-06-15",
         open_price=150.0,
         high_price=155.0,
         low_price=149.0,
         close_price=153.0,
         volume=1000000,
         volume_weighted_average_price=152.5,
     )

67-81: Message substring assertions look good; consider adding a malformed-date test

Asserting substrings keeps this resilient to Pydantic’s error wrappers. Consider adding a test that an invalid date string raises with the expected message to fully cover validate_timestamp.

Example to add elsewhere in this file:

def test_equity_bar_invalid_timestamp_string() -> None:
    with pytest.raises(ValidationError) as exc_info:
        EquityBar(
            ticker="AAPL",
            timestamp="2023/15/01",  # invalid format for the model
            open_price=150.0,
            high_price=155.0,
            low_price=149.0,
            close_price=153.0,
            volume=1000000,
            volume_weighted_average_price=152.5,
        )
    assert "Invalid date format" in str(exc_info.value)
libraries/python/tests/test_dates.py (1)

29-33: Use a string input to cover the string-parsing path (test name says “string”)

This test currently passes a date object. Pass a string to match the name and to exercise the format parsing logic.

Apply this diff:

-def test_date_string_dash_format() -> None:
-    date_instance = Date(date=datetime.date.fromisoformat("2023-03-15"))
+def test_date_string_dash_format() -> None:
+    date_instance = Date(date="2023-03-15")  # type: ignore
libraries/python/src/internal/mhsa_network.py (1)

61-64: Optional: expose a training flag to control dropout

Consider a self.training flag (and/or a forward arg) to toggle dropout deterministically in tests/inference without changing the rate.

I can sketch a minimal training/eval toggle if you want it in this PR.

libraries/python/tests/test_variable_selection_network.py (2)

6-7: Seed RNG for determinism across runs

Seeding avoids sporadic failures in environments that analyze exact numeric thresholds or aggregate stats.

Apply this diff:

-rng = Generator(PCG64())
+rng = Generator(PCG64(0))

18-23: Weight-shape assertions may be brittle against tinygrad changes

tinygrad.nn.Linear’s internal weight layout can vary between (out, in) and (in, out) across versions. These checks may break on upgrades.

Consider replacing weight-shape assertions with behavioral checks (e.g., feed (B, input_dimension) and assert output shape is (B, input_dimension), which you already do below), or gate these asserts behind a detected layout:

# example approach (no diff applied):
wshape = vsn.input_layer.weight.shape
assert input_dimension in wshape and hidden_size in wshape

Would you like me to adjust the tests to be layout-agnostic?

applications/datamanager/src/datamanager/main.py (2)

255-261: Avoid overriding CloudEvent helper’s timestamp; remove duplicate “date”.

create_cloud_event_success already injects a timestamp in data. Passing your own date key overrides it. If overriding is not intentional, drop the custom date field for consistency.

Apply this diff:

-        data={
-            "date": request_fetch_date,
-            "count": count,
-        },
+        data={"count": count},

294-333: Prefer specific exception handling for calendar endpoint.

Catching bare Exception hides actionable errors (API/HTTP/validation) and makes observability harder. Handle known exceptions and return 500 with clearer messages.

Apply this diff:

-    except Exception as e:  # noqa: BLE001
+    except (requests.RequestException, Exception) as e:  # replace Exception with specific Alpaca exceptions if available
         logger.error(f"Error fetching calendar data: {e}")
         logger.error(traceback.format_exc())
         return Response(
             status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
             content=json.dumps({"error": f"Failed to fetch calendar data: {e!s}"}),
             media_type="application/json",
         )
applications/positionmanager/tests/test_positionmanager_main.py (1)

21-24: Patch target for requests.get is unnecessary here.

DataClient.get_data is already mocked, so requests.get inside the client won’t be called. This patch can be removed to reduce noise.

Apply this diff:

-    @patch("applications.positionmanager.src.positionmanager.main.requests.get")
     def test_open_position_success(
         self,
-        mock_requests_get: MagicMock,
         MockDataClient: MagicMock,  # noqa: N803
         MockAlpacaClient: MagicMock,  # noqa: N803
     ) -> None:
libraries/python/src/internal/equity_bar.py (1)

40-49: Accepting only YYYY-MM-DD strings is fine; mention it in the field description or reuse internal Date parser.

Current validator is strict ISO date. If you want to accept “YYYY/MM/DD” like internal.Date, reuse that parser for consistency; otherwise, clarify the format in the docstring.

Apply this diff to clarify the description:

-    timestamp: date = Field(..., description="The date of the bar in YYYY-MM-DD format")
+    timestamp: date = Field(..., description="The date of the bar in YYYY-MM-DD format (ISO 8601)")
applications/models/src/models/train_tft_model.py (1)

143-165: Hard-coded training CSV path; consider parameterizing for portability.

Embedding a repo-relative path reduces reusability in Flyte. Expose filepath as a workflow input or env var.

Apply this diff to accept a filepath:

-@workflow
-def train_tft_model() -> None:
-    dataset = read_local_data(
-        filepath="applications/models/src/models/training_data.csv"
-    )  # type: ignore[assignment]
+@workflow
+def train_tft_model(filepath: str = "applications/models/src/models/training_data.csv") -> None:
+    dataset = read_local_data(filepath=filepath)  # type: ignore[assignment]
applications/positionmanager/src/positionmanager/main.py (1)

245-256: Avoid duplicating timestamp field; create_cloud_event_success already adds one.

Remove redundant "date" key from data to keep payloads consistent across apps.

Apply this diff:

     return create_cloud_event_success(
         application_name="positionmanager",
         event_metadata=["positions", "open"],
         data={
-            "date": date_range.end.isoformat(),
             "initial_cash_balance": float(cash_balance),
             "final_cash_balance": float(final_cash_balance),
             "optimized_portfolio": optimized_portfolio,
             "executed_trades": executed_trades,
             "time_period": date_range.to_object(),
         },
     )
libraries/python/src/internal/tft_model.py (2)

85-90: Nit: parameter naming (attention_head_size)

This parameter represents a count, not a size. Consider renaming to attention_head_count in Parameters and here for clarity and consistency with MultiHeadSelfAttentionNetwork(heads_count=...). This is a naming-only suggestion; not a functional issue.


247-260: State serialization LGTM

The save/load APIs are consistent with tinygrad’s state utils. Optional: check return value of load_state_dict for diagnostics.

If desired, capture and log the return for visibility:

-        states = safe_load(path_and_file)
-        load_state_dict(self, states)
+        states = safe_load(path_and_file)
+        _ = load_state_dict(self, states)

Comment thread .claude/settings.local.json Outdated
Comment thread .github/workflows/teardown_infrastructure.yaml Outdated
Comment thread applications/datamanager/pyproject.toml Outdated
Comment thread applications/datamanager/src/datamanager/clients.py Outdated
Comment thread applications/datamanager/src/datamanager/clients.py Outdated
Comment on lines +39 to +45
self.hidden_size = parameters.hidden_size
self.batch_size = parameters.input_length
self.output_length = parameters.output_length
self.output_size = parameters.output_size
self.quantiles = parameters.quantiles

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

self.batch_size is incorrectly set to a sequence length (input_length)

Batch size is dynamic per batch and should be inferred from inputs in forward. Setting it to parameters.input_length causes downstream shape bugs (decoder zeros shape, static context view, context expansion, quantiles reshape).

Remove the incorrect attribute and compute batch_size and sequence lengths inside forward.

-        self.batch_size = parameters.input_length

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In libraries/python/src/internal/tft_model.py around lines 39 to 44, remove the
incorrect assignment self.batch_size = parameters.input_length and any other
places that rely on this static attribute; instead compute batch_size and
sequence lengths inside forward by reading them from the input tensors (e.g.
batch_size = x.size(0), input_length = x.size(1) or equivalent), update decoder
zero tensors, static context view expansions, context expansion logic, and
quantiles reshape calls to use these local values, and delete references to
self.batch_size throughout the class so shapes are derived at runtime per-batch.

Comment on lines +96 to +100
self.output_layer = Linear(
in_features=parameters.hidden_size,
out_features=parameters.output_size,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Output layer must emit per-quantile outputs

You later reshape to include a quantile dimension but the layer currently produces only output_size units. This will make the reshape invalid or silently wrong. Make the output size output_size * len(quantiles).

-        self.output_layer = Linear(
-            in_features=parameters.hidden_size,
-            out_features=parameters.output_size,
-        )
+        self.output_layer = Linear(
+            in_features=parameters.hidden_size,
+            out_features=parameters.output_size * len(parameters.quantiles),
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.output_layer = Linear(
in_features=parameters.hidden_size,
out_features=parameters.output_size,
)
self.output_layer = Linear(
in_features=parameters.hidden_size,
out_features=parameters.output_size * len(parameters.quantiles),
)
🤖 Prompt for AI Agents
In libraries/python/src/internal/tft_model.py around lines 96 to 99, the output
Linear layer currently uses out_features=parameters.output_size but downstream
code reshapes outputs to include a quantile dimension; change out_features to
output_size multiplied by the number of quantiles (e.g. parameters.output_size *
len(parameters.quantiles) or parameters.output_size * len(self.quantiles)
depending on where quantiles are stored) so the produced tensor can be reshaped
correctly; ensure the quantiles list is accessible at this point and update any
related comments or docs to reflect the per-quantile output shape.

Comment on lines +129 to +137
static_input = static_categorical_features.to(
device=self.static_context_linear.weight.device
)

static_context = self.static_context_linear(static_input)

static_context = static_context.view((self.batch_size, self.hidden_size))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Static context shaping bug; avoid hard-coded view with self.batch_size

static_categorical_features likely has shape (batch, 1, static_dim) or (batch, static_dim). Use squeeze(1) if needed and avoid using self.batch_size in view.

-        static_input = static_categorical_features.to(
-            device=self.static_context_linear.weight.device
-        )
-
-        static_context = self.static_context_linear(static_input)
-
-        static_context = static_context.view((self.batch_size, self.hidden_size))
+        static_input = static_categorical_features.to(
+            device=self.static_context_linear.weight.device
+        )
+        if len(static_input.shape) == 3 and static_input.shape[1] == 1:
+            static_input = static_input.squeeze(1)
+        static_context = self.static_context_linear(static_input)  # (batch_size, hidden_size)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static_input = static_categorical_features.to(
device=self.static_context_linear.weight.device
)
static_context = self.static_context_linear(static_input)
static_context = static_context.view((self.batch_size, self.hidden_size))
static_input = static_categorical_features.to(
device=self.static_context_linear.weight.device
)
if len(static_input.shape) == 3 and static_input.shape[1] == 1:
static_input = static_input.squeeze(1)
static_context = self.static_context_linear(static_input) # (batch_size, hidden_size)
🤖 Prompt for AI Agents
In libraries/python/src/internal/tft_model.py around lines 129 to 136, the
static_context shaping uses a hard-coded self.batch_size which can fail when
static_categorical_features has shape (batch, 1, static_dim) or (batch,
static_dim); instead remove the fixed self.batch_size, squeeze the needless
dimension if present (e.g. .squeeze(1) when second dim == 1) and reshape/view
based on the tensor's actual first dimension or use .view(-1, self.hidden_size)
(or .reshape(-1, self.hidden_size)) to produce (batch, hidden_size); ensure you
perform the squeeze before moving to device/linear if ordering matters and
preserve device consistency.

Comment on lines +81 to +134
def test_dataset_batches() -> None:
data = pl.DataFrame(
{
"timestamp": [
1672531200000, # 2023-01-01
1672617600000, # 2023-01-02
1672704000000, # 2023-01-03
],
"open_price": [100.0, 101.0, 102.0],
"high_price": [110.0, 111.0, 112.0],
"low_price": [90.0, 91.0, 92.0],
"close_price": [105.0, 106.0, 107.0],
"volume": [1000.0, 1100.0, 1200.0],
"volume_weighted_average_price": [105.0, 106.0, 107.0],
"ticker": ["AAPL", "AAPL", "AAPL"],
"sector": ["Technology", "Technology", "Technology"],
"industry": [
"Consumer Electronics",
"Consumer Electronics",
"Consumer Electronics",
],
"is_holiday": [True, False, False],
}
)

dataset = TemporalFusionTransformerDataset(data=data)

expected_input_length = 2
expected_output_length = 1

batches = dataset.get_batches(
data_type="predict",
input_length=expected_input_length,
output_length=expected_output_length,
)

assert isinstance(batches, list)
assert len(batches) == 1

for batch in batches:
assert "encoder_categorical_features" in batch
assert "encoder_continuous_features" in batch
assert "decoder_categorical_features" in batch
assert "static_categorical_features" in batch

encoder_categorical_features = batch["encoder_categorical_features"]
encoder_continuous_features = batch["encoder_continuous_features"]
decoder_categorical_features = batch["decoder_categorical_features"]
static_categorical_features = batch["static_categorical_features"]

assert encoder_categorical_features.shape[0] == expected_input_length
assert encoder_continuous_features.shape[0] == expected_input_length
assert decoder_categorical_features.shape[0] == expected_output_length
assert static_categorical_features.shape[0] == 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding tests for train and validate data types.

The current test only covers data_type="predict". Consider adding test cases for "train" and "validate" modes to ensure complete coverage of the get_batches method.

Would you like me to generate additional test cases for the "train" and "validate" data types to ensure comprehensive coverage of the batch generation functionality?

🤖 Prompt for AI Agents
In libraries/python/tests/test_dataset.py around lines 81 to 134, the test only
exercises get_batches for data_type="predict"; add two additional subtests
calling dataset.get_batches with data_type="train" and data_type="validate"
(using the same expected_input_length and expected_output_length) and perform
the same assertions as the existing predict test: verify the result is a list,
non-empty (expecting one batch for this small fixture), and for each batch
assert the presence of "encoder_categorical_features",
"encoder_continuous_features", "decoder_categorical_features", and
"static_categorical_features" and that their row counts equal
expected_input_length, expected_input_length, expected_output_length, and 1
respectively.

Comment thread libraries/python/tests/test_loss_functions.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines +20 to +26
hidden_size: int = 64
output_size: int = 1 # closing price
lstm_layer_count: int = 3
attention_head_size: int = 4
dropout_rate: float = 0.1
quantiles: list[float] = [0.1, 0.5, 0.9]
decoder_categorical_dimension: int
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid mutable default for quantiles in a Pydantic model

Using a list literal as a default creates a shared mutable default across instances. Prefer Field(default_factory=...).

Apply this diff:

-from pydantic import BaseModel
+from pydantic import BaseModel, Field
@@
-    quantiles: list[float] = [0.1, 0.5, 0.9]
+    quantiles: list[float] = Field(default_factory=lambda: [0.1, 0.5, 0.9])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
hidden_size: int = 64
output_size: int = 1 # closing price
lstm_layer_count: int = 3
attention_head_size: int = 4
dropout_rate: float = 0.1
quantiles: list[float] = [0.1, 0.5, 0.9]
decoder_categorical_dimension: int
from pydantic import BaseModel, Field
hidden_size: int = 64
output_size: int = 1 # closing price
lstm_layer_count: int = 3
attention_head_size: int = 4
dropout_rate: float = 0.1
quantiles: list[float] = Field(default_factory=lambda: [0.1, 0.5, 0.9])
decoder_categorical_dimension: int
🤖 Prompt for AI Agents
In libraries/python/src/internal/tft_model.py around lines 20 to 26, the
Pydantic model uses a list literal for the default of `quantiles`, which is a
mutable shared default; change it to use Field with a default_factory to return
a new list per instance (e.g. import Field from pydantic and replace the literal
with Field(default_factory=lambda: [0.1, 0.5, 0.9])) so each model instance gets
its own list.

Comment on lines +103 to +114
def forward(self, inputs: dict[str, Tensor]) -> dict[str, Tensor]:
# NOTE: rename all variables
# NOTE: potentially remove unused variables
encoder_categorical_features = inputs["encoder_categorical_features"]
encoder_continuous_features = inputs["encoder_continuous_features"]

encoder_input = encoder_categorical_features.cat(
encoder_continuous_features,
dim=2,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Derive dynamic batch/sequence sizes at runtime

Compute batch_size and encoder sequence length from the provided inputs; they are used repeatedly in this method.

         encoder_categorical_features = inputs["encoder_categorical_features"]
         encoder_continuous_features = inputs["encoder_continuous_features"]
 
+        # Dynamic sizes
+        batch_size = encoder_categorical_features.shape[0]
+        encoder_seq_len = encoder_categorical_features.shape[1]
+
         encoder_input = encoder_categorical_features.cat(
             encoder_continuous_features,
             dim=2,
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def forward(self, inputs: dict[str, Tensor]) -> dict[str, Tensor]:
# NOTE: rename all variables
# NOTE: potentially remove unused variables
encoder_categorical_features = inputs["encoder_categorical_features"]
encoder_continuous_features = inputs["encoder_continuous_features"]
encoder_input = encoder_categorical_features.cat(
encoder_continuous_features,
dim=2,
)
def forward(self, inputs: dict[str, Tensor]) -> dict[str, Tensor]:
# NOTE: rename all variables
# NOTE: potentially remove unused variables
encoder_categorical_features = inputs["encoder_categorical_features"]
encoder_continuous_features = inputs["encoder_continuous_features"]
# Dynamic sizes
batch_size = encoder_categorical_features.shape[0]
encoder_seq_len = encoder_categorical_features.shape[1]
encoder_input = encoder_categorical_features.cat(
encoder_continuous_features,
dim=2,
)
🤖 Prompt for AI Agents
In libraries/python/src/internal/tft_model.py around lines 103 to 113, the
method currently doesn't derive dynamic dimensions and repeatedly assumes sizes;
compute batch_size = inputs["encoder_categorical_features"].size(0) and
encoder_seq_len = inputs["encoder_categorical_features"].size(1) (or equivalent
based on tensor shape) at runtime and use these variables wherever batch or
encoder sequence length is needed in the method, replacing hard-coded or
repeatedly accessed size calls to improve clarity and avoid redundant indexing.

Comment on lines +114 to +124
decoder_categorical_features = inputs["decoder_categorical_features"]
decoder_continuous_features = Tensor.zeros(
self.batch_size, decoder_categorical_features.shape[1], 0
) # not currently used

decoder_input = decoder_categorical_features.cat(
decoder_continuous_features,
dim=2,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix decoder zeros shape; first dimension must be batch size (not input length)

Currently uses self.batch_size which is wrong. Also capture decoder sequence length.

         decoder_categorical_features = inputs["decoder_categorical_features"]
-        decoder_continuous_features = Tensor.zeros(
-            self.batch_size, decoder_categorical_features.shape[1], 0
-        )  # not currently used
+        decoder_seq_len = decoder_categorical_features.shape[1]
+        decoder_continuous_features = Tensor.zeros(
+            batch_size, decoder_seq_len, 0
+        )  # not currently used

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In libraries/python/src/internal/tft_model.py around lines 114 to 123, the zero
tensor uses self.batch_size incorrectly; derive batch_size and decoder_seq_len
from decoder_categorical_features (batch_size =
decoder_categorical_features.shape[0], decoder_seq_len =
decoder_categorical_features.shape[1]) and create the zeros tensor with shape
(batch_size, decoder_seq_len, 0) (or use a zeros_like approach for clarity)
before concatenating along dim=2 so the first two dimensions match
decoder_categorical_features.

Comment on lines +145 to +152
encoder_static_context = static_context.unsqueeze(1).expand(
-1, self.batch_size, -1
)

decoder_static_context = static_context.unsqueeze(1).expand(
-1, self.output_length, -1
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use dynamic sequence lengths for static context expansion

self.batch_size is not a sequence length. Expand over encoder/decoder sequence lengths.

-        encoder_static_context = static_context.unsqueeze(1).expand(
-            -1, self.batch_size, -1
-        )
+        encoder_static_context = static_context.unsqueeze(1).expand(
+            -1, encoder_seq_len, -1
+        )
 
-        decoder_static_context = static_context.unsqueeze(1).expand(
-            -1, self.output_length, -1
-        )
+        decoder_static_context = static_context.unsqueeze(1).expand(
+            -1, decoder_seq_len, -1
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
encoder_static_context = static_context.unsqueeze(1).expand(
-1, self.batch_size, -1
)
decoder_static_context = static_context.unsqueeze(1).expand(
-1, self.output_length, -1
)
encoder_static_context = static_context.unsqueeze(1).expand(
-1, encoder_seq_len, -1
)
decoder_static_context = static_context.unsqueeze(1).expand(
-1, decoder_seq_len, -1
)
🤖 Prompt for AI Agents
In libraries/python/src/internal/tft_model.py around lines 145 to 151, the
static context is being expanded using self.batch_size for the encoder sequence
dimension which is incorrect; change the encoder expansion to use the encoder
sequence length (e.g., self.input_length or the actual encoder tensor's sequence
length) so it becomes static_context.unsqueeze(1).expand(-1, encoder_seq_len,
-1). Leave the decoder expansion using self.output_length or similarly derive it
dynamically from the decoder/input tensors to ensure the expanded shapes match
encoder/decoder time dimensions.

Comment on lines +173 to +190
decoder_attended = attended_output[:, -self.output_length :, :]

output = self.pre_output_layer(decoder_attended).relu()

predictions = self.output_layer(output)

quantiles = predictions.reshape(
self.batch_size,
self.output_length,
self.output_size,
len(self.quantiles),
)

return {
"predictions": predictions, # shape: (batch_size, output_length, output_size) # noqa: E501
"quantiles": quantiles, # shape: (batch_size, output_length, output_size, len(quantiles)) # noqa: E501
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Quantile tensor construction and point predictions

  • Reshape uses self.batch_size and assumes output layer already expanded quantile dimension; both are incorrect as-is.
  • Provide a correct reshape with dynamic batch_size, and produce a point prediction (e.g., median) for convenience.
-        output = self.pre_output_layer(decoder_attended).relu()
-
-        predictions = self.output_layer(output)
-
-        quantiles = predictions.reshape(
-            self.batch_size,
-            self.output_length,
-            self.output_size,
-            len(self.quantiles),
-        )
-
-        return {
-            "predictions": predictions,  # shape: (batch_size, output_length, output_size)
-            "quantiles": quantiles,  # shape: (batch_size, output_length, output_size, len(quantiles))
-        }
+        output = self.pre_output_layer(decoder_attended).relu()
+
+        raw = self.output_layer(output)  # (batch_size, output_length, output_size * Q)
+        quantiles = raw.reshape(
+            batch_size,
+            self.output_length,
+            self.output_size,
+            len(self.quantiles),
+        )
+
+        # Use median quantile for point predictions if available
+        try:
+            median_index = self.quantiles.index(0.5)
+        except ValueError:
+            median_index = len(self.quantiles) // 2
+        predictions = quantiles[:, :, :, median_index]
+
+        return {
+            "predictions": predictions,  # (batch_size, output_length, output_size)
+            "quantiles": quantiles,  # (batch_size, output_length, output_size, Q)
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
decoder_attended = attended_output[:, -self.output_length :, :]
output = self.pre_output_layer(decoder_attended).relu()
predictions = self.output_layer(output)
quantiles = predictions.reshape(
self.batch_size,
self.output_length,
self.output_size,
len(self.quantiles),
)
return {
"predictions": predictions, # shape: (batch_size, output_length, output_size) # noqa: E501
"quantiles": quantiles, # shape: (batch_size, output_length, output_size, len(quantiles)) # noqa: E501
}
decoder_attended = attended_output[:, -self.output_length :, :]
output = self.pre_output_layer(decoder_attended).relu()
raw = self.output_layer(output) # (batch_size, output_length, output_size * Q)
quantiles = raw.reshape(
batch_size,
self.output_length,
self.output_size,
len(self.quantiles),
)
# Use median quantile for point predictions if available
try:
median_index = self.quantiles.index(0.5)
except ValueError:
median_index = len(self.quantiles) // 2
predictions = quantiles[:, :, :, median_index]
return {
"predictions": predictions, # (batch_size, output_length, output_size)
"quantiles": quantiles, # (batch_size, output_length, output_size, Q)
}
🤖 Prompt for AI Agents
libraries/python/src/internal/tft_model.py around lines 173 to 189: the current
reshape hardcodes self.batch_size and assumes the output_layer already expanded
the quantile dimension; instead reshape dynamically by inferring the batch
dimension (use -1 for batch when calling reshape/view) to produce quantiles with
shape (batch, output_length, output_size, len(self.quantiles)), and add a
convenience point prediction returned (e.g., the median) by selecting the center
quantile along the last dimension; ensure you use self.output_length and
self.output_size for the other dimensions and len(self.quantiles) for the
quantile axis.

Comment on lines +207 to +217
loss = quantile_loss(
outputs["predictions"],
inputs["targets"],
self.quantiles,
)

optimizer.zero_grad()
_ = loss.backward()
optimizer.step()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect tensor shapes passed to quantile_loss

quantile_loss expects (batch, output_size, Q) predictions and (batch, output_size) targets. Your predictions currently lack a quantile dimension and include a time dimension. Use the quantiles tensor from forward and flatten the time dimension.

-                loss = quantile_loss(
-                    outputs["predictions"],
-                    inputs["targets"],
-                    self.quantiles,
-                )
+                Q = len(self.quantiles)
+                preds = outputs["quantiles"].reshape(-1, self.output_size, Q)
+                targets = inputs["targets"].reshape(-1, self.output_size)
+                loss = quantile_loss(
+                    preds,
+                    targets,
+                    self.quantiles,
+                )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
loss = quantile_loss(
outputs["predictions"],
inputs["targets"],
self.quantiles,
)
optimizer.zero_grad()
_ = loss.backward()
optimizer.step()
Q = len(self.quantiles)
preds = outputs["quantiles"].reshape(-1, self.output_size, Q)
targets = inputs["targets"].reshape(-1, self.output_size)
loss = quantile_loss(
preds,
targets,
self.quantiles,
)
optimizer.zero_grad()
_ = loss.backward()
optimizer.step()

Comment on lines +236 to +246
loss = quantile_loss(
outputs["predictions"],
inputs["targets"],
self.quantiles,
)

total_loss += loss.numpy().item()
batch_count += 1

return total_loss / batch_count
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validation loss uses wrong tensor shapes

Same issue as training: use quantiles and flatten time dimension before computing loss.

-            loss = quantile_loss(
-                outputs["predictions"],
-                inputs["targets"],
-                self.quantiles,
-            )
-
-            total_loss += loss.numpy().item()
+            Q = len(self.quantiles)
+            preds = outputs["quantiles"].reshape(-1, self.output_size, Q)
+            targets = inputs["targets"].reshape(-1, self.output_size)
+            loss = quantile_loss(
+                preds,
+                targets,
+                self.quantiles,
+            )
+            total_loss += loss.numpy().item()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
loss = quantile_loss(
outputs["predictions"],
inputs["targets"],
self.quantiles,
)
total_loss += loss.numpy().item()
batch_count += 1
return total_loss / batch_count
Q = len(self.quantiles)
preds = outputs["quantiles"].reshape(-1, self.output_size, Q)
targets = inputs["targets"].reshape(-1, self.output_size)
loss = quantile_loss(
preds,
targets,
self.quantiles,
)
total_loss += loss.numpy().item()
batch_count += 1
return total_loss / batch_count
🤖 Prompt for AI Agents
In libraries/python/src/internal/tft_model.py around lines 236 to 245, the
validation loss is computed with mismatched tensor shapes; reshape the tensors
to flatten the time dimension and align the quantile dimension before calling
quantile_loss — specifically permute/reshape outputs["predictions"] to
(batch*time, num_quantiles) and flatten inputs["targets"] to (batch*time,) (or
expand to (batch*time, num_quantiles) if quantile_loss expects that), ensure
quantiles are broadcastable to the prediction shape, then call quantile_loss,
convert the resulting tensor to a scalar and update total_loss and batch_count
as before.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 39

🔭 Outside diff range comments (7)
infrastructure/environment_variables.py (1)

20-32: Preserve secret semantics and add a temporary alias for backward compatibility.

Wrap the final mapping in Output.secret to ensure the entire map is treated as a secret and not logged. Also add ALPACA_API_KEY as an alias to reduce rollout risk; you can remove it after all consumers migrate.

-    return pulumi.Output.all(
-        [
-            ("ALPACA_API_KEY_ID", alpaca_api_key_id),
-            ("ALPACA_API_SECRET", alpaca_api_secret),
-            ("DATA_BUCKET_NAME", data_bucket_name),
-            ("POLYGON_API_KEY", polygon_api_key),
-            ("DUCKDB_ACCESS_KEY", duckdb_access_key),
-            ("DUCKDB_SECRET", duckdb_secret),
-            ("AWS_REGION", aws_region),
-            ("DUCKDB_USER_ACCESS_KEY_ID", duckdb_user_access_key.id),
-            ("DUCKDB_USER_ACCESS_KEY_SECRET", duckdb_user_access_key.secret),
-        ]
-    ).apply(lambda secrets: dict(secrets))
+    return pulumi.Output.secret(
+        pulumi.Output.all(
+            [
+                ("ALPACA_API_KEY_ID", alpaca_api_key_id),
+                # Temporary alias to ease migration; remove after downstream updates.
+                ("ALPACA_API_KEY", alpaca_api_key_id),
+                ("ALPACA_API_SECRET", alpaca_api_secret),
+                ("DATA_BUCKET_NAME", data_bucket_name),
+                ("POLYGON_API_KEY", polygon_api_key),
+                ("DUCKDB_ACCESS_KEY", duckdb_access_key),
+                ("DUCKDB_SECRET", duckdb_secret),
+                ("AWS_REGION", aws_region),
+                ("DUCKDB_USER_ACCESS_KEY_ID", duckdb_user_access_key.id),
+                ("DUCKDB_USER_ACCESS_KEY_SECRET", duckdb_user_access_key.secret),
+            ]
+        ).apply(lambda secrets: dict(secrets))
+    )
applications/datamanager/pyproject.toml (1)

6-19: Action: Add alpaca-py to applications/datamanager/pyproject.toml

datamanager imports alpaca.trading (applications/datamanager/src/datamanager/clients.py) but applications/datamanager/pyproject.toml doesn't declare alpaca-py — this will raise ImportError at runtime. I verified the repo already declares alpaca-py in applications/positionmanager/pyproject.toml ("alpaca-py>=0.15.0") and uv.lock pins alpaca-py to 0.42.0; pick the repo-wide constraint you prefer.

Files to update:

  • applications/datamanager/pyproject.toml — add dependency
  • applications/datamanager/src/datamanager/clients.py — (imports alpaca.trading)

Suggested diff (align version with positionmanager or uv.lock as desired):

 dependencies = [
   "fastapi>=0.115.12",
   "uvicorn>=0.34.2",
   "duckdb>=1.2.2",
   "polars>=1.29.0",
   "pyarrow>=20.0.0",
   "loguru>=0.7.3",
   "requests>=2.31.0",
   "prometheus-fastapi-instrumentator>=7.1.0",
   "cloudevents>=1.12.0",
   "polygon-api-client>=1.14.6",
   "boto3>=1.38.23",
+  "alpaca-py>=0.15.0",
   "internal==0.1.0",
 ]
libraries/python/src/internal/dates.py (1)

38-49: Fix validator type annotations and avoid redundant @classmethod.

The end validator is declared on a date field but typed as datetime.datetime. Also, field_validator returns a classmethod in Pydantic v2; the explicit @classmethod is redundant and unconventional.

Apply:

-    @field_validator("end")
-    @classmethod
-    def check_end_after_start(
-        cls,
-        end_value: datetime.datetime,
-        info: core_schema.ValidationInfo,
-    ) -> datetime.datetime:
+    @field_validator("end")
+    def check_end_after_start(
+        cls,
+        end_value: datetime.date,
+        info: core_schema.ValidationInfo,
+    ) -> datetime.date:
         start_value = info.data.get("start")
         if start_value and end_value <= start_value:
             msg = "End date must be after start date."
             raise ValueError(msg)
         return end_value
pyproject.toml (1)

22-28: Pytest rootdir points to an absolute path that likely doesn't exist

"--rootdir=/tests" will set the repository root for pytest to a non-existent absolute directory on most CI/dev setups, breaking discovery/imports. You already set testpaths; remove rootdir to let pytest infer the correct project root.

 addopts = [
     "--verbose",
     "--tb=short",
     "--strict-markers",
     "--color=yes",
-    "--rootdir=/tests",
 ]
libraries/python/src/internal/lstm_network.py (1)

68-72: Dropout is applied to the wrong tensor and toggles training mode unexpectedly

  • Dropout should be applied to the layer’s output (layer_hidden_state), not directly to hidden_state[index] after it’s stored.
  • Calling .train() on a Tensor here is surprising and leaves training state altered for downstream ops.

Refactor to apply dropout to layer_hidden_state without mutating training mode.

-                if self.dropout_rate > 0.0 and index < self.layer_count - 1:
-                    hidden_state[index].train()
-                    hidden_state[index] = hidden_state[index].dropout(self.dropout_rate)
+                if self.dropout_rate > 0.0 and index < self.layer_count - 1:
+                    layer_hidden_state = layer_hidden_state.dropout(self.dropout_rate)
+
+                # propagate the (possibly dropped-out) hidden state forward and into states
+                hidden_state[index] = layer_hidden_state
+                cell_state[index] = layer_cell_state

Note: This moves state assignment after optional dropout to keep state and forward input consistent.

Also applies to: 56-66

applications/datamanager/src/datamanager/main.py (1)

179-185: Fix Content-Disposition header and Arrow media type

  • f-string debug syntax {filename=} yields Content-Disposition: attachment; filename='…' which is invalid.
  • You’re writing Arrow IPC stream (RecordBatchStreamWriter), so media type should be application/vnd.apache.arrow.stream.

Apply this patch:

-            filename = f"equity_bars_{start_date}_{end_date}.arrow"
-            content_disposition = f"attachment; {filename=}"
+            filename = f"equity_bars_{start_date}_{end_date}.arrow"
+            content_disposition = f'attachment; filename="{filename}"'
 ...
-        return Response(
+        return Response(
             content=sink.getvalue().to_pybytes(),
-            media_type="application/vnd.apache.arrow.file",
+            media_type="application/vnd.apache.arrow.stream",
             headers={
                 "Content-Disposition": content_disposition,
                 "X-Row-Count": str(data.num_rows),
                 "X-Start-Date": str(start_date),
                 "X-End-Date": str(end_date),
             },
         )
applications/positionmanager/pyproject.toml (1)

5-5: Python pin (==3.12.10) conflicts with test image (Python 3.13) — fix required

Short: Multiple pyproject.toml files (and uv.lock) pin requires-python = "==3.12.10", while the test images use Python 3.13 — this will cause lock/uv resolution failures in CI/tests.

Files to update / check:

  • pyproject.toml (root) — requires-python = "==3.12.10"
  • libraries/python/pyproject.toml — requires-python = "==3.12.10"
  • applications/positionmanager/pyproject.toml — requires-python = "==3.12.10"
  • applications/predictionengine/pyproject.toml — requires-python = "==3.12.10"
  • applications/datamanager/pyproject.toml — requires-python = "==3.12.10"
  • applications/models/pyproject.toml — requires-python = "==3.12.10"
  • infrastructure/pyproject.toml — requires-python = "==3.12.10"
  • uv.lock — requires-python = "==3.12.10" (must be regenerated after changes)
  • Dockerfiles using Python 3.13:
    • Dockerfile.tests (root): FROM python:3.13
    • applications/datamanager/Dockerfile.test: FROM python:3.13-slim

Proposed change (preferred):
requires-python = ">=3.12,<3.14"

After making the pin change in all pyproject.toml files, regenerate the lockfile (e.g., run the project’s uv sync command used in Dockerfile.tests) and re-run tests.

Alternative: align test images to Python 3.12.10 (change Dockerfile.tests and any test images) instead of loosening the pyproject pins.

♻️ Duplicate comments (4)
applications/models/pyproject.toml (1)

7-19: Remove duplicate internal dependency; consider relying on the workspace source.

The dependency "internal==0.1.0" is listed twice, which can cause resolver noise. Also, since you’ve configured [tool.uv.sources] to use the workspace, you can avoid pinning or relax to a range to prevent version skew between the workspace and this app.

Apply this minimal fix:

 dependencies = [
-    "internal==0.1.0",
+    "internal==0.1.0",
     "boto3>=1.38.23",
     "botocore>=1.38.23",
     "requests>=2.31.0",
     "pyarrow>=20.0.0",
     "polygon-api-client>=1.14.6",
-    "internal==0.1.0",
     "flytekit>=1.16.1",
     "polars>=1.29.0",
     "loguru>=0.7.3",
     "pydantic>=2.8.2",
     "wandb>=0.21.1",
 ]

Optional follow-ups:

  • Prefer unpinned internal to align with the workspace source:
    • Replace "internal==0.1.0" with "internal" or "internal>=0.1.0".
  • Consider relaxing requires-python to a minor range (e.g., ">=3.12,<3.13") unless you have a strict patch-level constraint.
libraries/python/src/internal/loss_functions.py (1)

16-26: Bug: accumulation doesn’t update errors_total (non-inplace add).

Tensor.add likely returns a new Tensor; the current code discards the result, yielding an incorrect (always-zero) loss.

Apply this fix:

-    errors_total = Tensor.zeros((1,))
+    errors_total = Tensor.zeros((1,))
     for index, quantile in enumerate(quantiles):
         error = targets.sub(predictions[:, :, index])
         quantile_tensor = Tensor([quantile])
-        errors_total.add(
+        errors_total = errors_total.add(
             Tensor.where(
                 error > 0,
                 quantile_tensor.mul(error),
                 (quantile_tensor.sub(1)).mul(error),
             ).mean()
         )
libraries/python/src/internal/lstm_network.py (1)

76-81: Stacking outputs will fail/behave oddly for a single timestep; handle len==1 safely

Using Tensor.stack(outputs[0], *outputs[1:], dim=1) assumes ≥1 outputs and relies on tinygrad’s varargs semantics; single-element behavior can be inconsistent and the current pattern is brittle. Stack from the whole list and special-case len==1 to unsqueeze.

-        output_tensor = Tensor.stack(outputs[0], *outputs[1:], dim=1)
+        if len(outputs) == 1:
+            output_tensor = outputs[0].unsqueeze(dim=1)
+        else:
+            output_tensor = Tensor.stack(*outputs, dim=1)
applications/datamanager/src/datamanager/main.py (1)

266-281: Don’t return 404 on S3 listing exceptions; distinguish errors from “not found”

An exception while listing objects is an operational error (500), not a “no data” condition (404). Only return 404 when the list is empty without exceptions.

-    try:
-        blobs = list(s3_client.list_objects(prefix=prefix))
-    except Exception as e:  # noqa: BLE001
-        logger.error(f"Error listing S3 objects: {e}")
-        return Response(
-            status_code=status.HTTP_404_NOT_FOUND,
-            content=json.dumps({"error": "No equity bars found"}),
-            media_type="application/json",
-        )
+    try:
+        blobs = list(s3_client.list_objects(prefix=prefix))
+    except Exception as e:  # noqa: BLE001
+        logger.error(f"Error listing S3 objects: {e}")
+        return Response(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            content=json.dumps({"error": f"Failed to list equity bars: {e!s}"}),
+            media_type="application/json",
+        )

Optionally catch boto3’s ClientError explicitly if available.

🧹 Nitpick comments (48)
.gitignore (1)

6-8: Scope CSV and wandb ignores to avoid masking legitimate data files.

Top-level ignores for *.csv and wandb/ can inadvertently hide intentionally tracked assets elsewhere in the repo. Recommend scoping to the models app (or a dedicated data/output directory) to prevent accidental omissions.

Apply this diff to narrow the patterns:

-*.egg-info
-wandb/
-*.csv
+*.egg-info
+# Scope experiment artifacts to models app only (adjust as needed)
+applications/models/wandb/
+applications/models/**/*.csv

Optional:

  • If you want to allow CSVs in repo-wide docs/examples, consider adding exceptions with negate patterns (!docs/**/*.csv).

Also applies to: 10-10, 19-21

applications/datamanager/pyproject.toml (1)

18-18: Avoid hard pinning the internal workspace dependency to reduce churn.

You’re already sourcing internal from the uv workspace; pinning to an exact version adds friction when you bump the library.

Apply one of the following:

  • Prefer letting the workspace source define the version:
-    "internal==0.1.0",
+    "internal",
  • Or allow forward-compatible bumps:
-    "internal==0.1.0",
+    "internal>=0.1.0",
libraries/python/src/internal/dates.py (1)

8-31: Date model looks good — add backward-compatibility alias (SummaryDate → Date)

Renaming SummaryDate → Date is a breaking change. I searched the repo for "SummaryDate" (no matches found), but absence of matches here doesn't guarantee no external consumers — add a soft-landing alias if downstream code may reference the old name.

  • File to update:
    • libraries/python/src/internal/dates.py — add alias at the bottom of the module

Suggested diff:

+# Backward-compatibility alias (remove after migration)
+SummaryDate = Date
libraries/python/pyproject.toml (1)

1-12: Right direction for a shared internal library; consider optionalizing heavy deps.

This centralizes shared code under uv. To reduce install footprint for services not using ML, consider moving tinygrad (and possibly numpy) under an optional dependency group, e.g., [project.optional-dependencies]. Keep pydantic, cloudevents, polars as core if widely used.

Example:

 [project]
 name = "internal"
 version = "0.1.0"
 description = "Shared Python resources"
 requires-python = "==3.12.10"
-dependencies = [
-    "pydantic>=2.8.2",
-    "cloudevents>=1.12.0",
-    "tinygrad>=0.10.3",
-    "numpy>=2.2.6",
-    "polars>=1.29.0",
-]
+dependencies = [
+    "pydantic>=2.8.2",
+    "cloudevents>=1.12.0",
+    "polars>=1.29.0",
+]
+
+[project.optional-dependencies]
+ml = [
+    "numpy>=2.2.6",
+    "tinygrad>=0.10.3",
+]

If you make this change, ensure packages that require ML features add internal[ml] to their dependencies.

.github/workflows/teardown_infrastructure.yaml (1)

1-9: Clarify the schedule comment for DST and simplify the if guard (optional).

  • The comment says “6:00 PM EST,” but 23:00 UTC is 7:00 PM during EDT. Prefer “ET” or specify both.
  • The if condition is redundant for a workflow that only has a single schedule; it’s harmless but can be removed.

Apply:

 name: Teardown infrastructure
 on:
   schedule:
-    - cron: '0 23 * * 1,2,3,4,5' # teardown at 6:00 PM EST
+    - cron: '0 23 * * 1,2,3,4,5' # 23:00 UTC (6pm ET standard / 7pm EDT)
 jobs:
   teardown_infrastructure:
-    name: Teardown infrastructure on weekday schedule
-    if: github.event.schedule == '0 23 * * 1,2,3,4,5'
+    name: Teardown infrastructure on weekday schedule
.github/workflows/launch_infrastructure.yaml (1)

2-8: Cron comment references EST; consider DST implications.

The cron at Line 5 runs at 13:00 UTC, which is 8:00 AM EST but 9:00 AM EDT. If you intend 8:00 AM local ET year-round, update the comment and/or cron strategy to avoid confusion.

libraries/python/tests/test_lstm_network.py (1)

39-47: Strengthen state validation in tests.

Optionally assert that each state list matches layer_count to catch regressions (e.g., hidden/cell state list length).

You can extend assertions like this:

-    assert isinstance(hidden_state, tuple)
-    assert len(hidden_state) == expected_hidden_state
+    assert isinstance(hidden_state, tuple)
+    assert len(hidden_state) == expected_hidden_state  # (hidden_state, cell_state)
+    hs, cs = hidden_state
+    assert isinstance(hs, list) and isinstance(cs, list)
+    assert len(hs) == lstm.layer_count
+    assert len(cs) == lstm.layer_count

Also applies to: 72-77, 87-91, 103-108

applications/models/src/models/get_alpaca_calendar.py (1)

33-37: Optional: allow output path override (CLI arg or env).

Hard-coding "calendar.csv" to CWD can be inconvenient in CI or when organizing datasets.

Example: accept an OUTPUT_PATH env var with a default.

-    calendar_data.write_csv("calendar.csv")
+    output_path = os.getenv("OUTPUT_PATH", "calendar.csv")
+    calendar_data.write_csv(output_path)
-    logger.info("Calendar data has been written to calendar.csv")
+    logger.info(f"Calendar data has been written to {output_path}")
libraries/python/src/internal/cloud_event.py (1)

7-11: Type generality and docstrings for maintainability.

Consider using Sequence[str] for event_metadata and add short docstrings clarifying attribute/type composition and timezone choice.

Also applies to: 24-28

libraries/python/src/internal/loss_functions.py (1)

16-28: Optional: vectorize across quantiles to reduce Python loop overhead.

You can stack quantiles into a Tensor and compute the piecewise loss in one pass.

Sketch:

# q: (Q,), error: (B, O, Q)
q = Tensor(quantiles)
error = targets.unsqueeze(-1).sub(predictions)
loss_q = Tensor.where(error > 0, q * error, (q - 1) * error).mean(axis=(0, 1))  # (Q,)
return loss_q.mean().reshape((1,))
libraries/python/src/internal/variable_selection_network.py (2)

1-2: Unify tinygrad imports for consistency with other internal modules

Other internal modules (e.g., mhsa_network.py) import Linear directly from tinygrad.nn. Consider aligning import style here to avoid mixing patterns across the codebase.

-from tinygrad import nn
-from tinygrad.tensor import Tensor
+from tinygrad.nn import Linear
+from tinygrad.tensor import Tensor

17-21: Add a runtime shape check and minimal docstring

Shape mismatches can propagate and be hard to debug later. A fast check keeps failures local. Adding a short docstring clarifies expected input/output shapes.

-    def forward(self, inputs: Tensor) -> Tensor:
+    def forward(self, inputs: Tensor) -> Tensor:
+        """
+        Args:
+            inputs: Tensor of shape (..., input_dimension)
+        Returns:
+            Tensor of shape (..., input_dimension) with values in [0, 1]
+        """
+        # Quick runtime validation
+        if inputs.shape[-1] != self.input_dimension:
+            raise ValueError(
+                f"Expected inputs.shape[-1] == {self.input_dimension}, got {inputs.shape[-1]}"
+            )
         inputs = self.input_layer(inputs)
         inputs = inputs.relu()
         inputs = self.output_layer(inputs)
         return inputs.sigmoid()
libraries/python/tests/test_mhsa_network.py (3)

8-15: Add a negative-case test for non-divisible head dimensions

This guards the constructor validation.

 def test_multi_head_attention_initialization() -> None:
     heads_count = 8
     embedding_size = 64
     attention = MultiHeadSelfAttentionNetwork(heads_count=8, embedding_size=64)
 
     assert attention.heads_count == heads_count
     assert attention.embedding_size == embedding_size
+
+
+def test_multi_head_attention_invalid_dims_raises() -> None:
+    # 30 is not divisible by 8
+    try:
+        _ = MultiHeadSelfAttentionNetwork(heads_count=8, embedding_size=30)
+        assert False, "Expected ValueError for invalid heads/embedding ratio"
+    except ValueError:
+        pass

17-29: Assert attention weights normalization across key dimension

Validates the softmax property: sums should be ~1 across the last axis.

 def test_multi_head_attention_forward() -> None:
     attention = MultiHeadSelfAttentionNetwork(heads_count=4, embedding_size=32)
 
     input_tensor = Tensor(rng.standard_normal((2, 10, 32)))
     output, attention_weights = attention.forward(input_tensor)
 
     batch_size = 2
     heads_count = 4
 
     assert output.shape == (batch_size, 10, 32)
     assert attention_weights.shape[0] == batch_size
     assert attention_weights.shape[1] == heads_count
+    # Softmax normalization: sum over keys dimension (-1) ≈ 1
+    sums = attention_weights.sum(axis=-1).numpy()
+    assert (abs(sums - 1.0) < 1e-5).all()

64-73: Exercise dropout path without changing shapes

A simple sanity check that enabling dropout preserves shapes and runs without error.

 def test_multi_head_attention_batch_processing() -> None:
     attention = MultiHeadSelfAttentionNetwork(heads_count=2, embedding_size=32)
 
     for batch_size in [1, 2, 4, 8]:
         input_tensor = Tensor(rng.standard_normal((batch_size, 5, 32)))
         output, attention_weights = attention.forward(input_tensor)
 
         assert output.shape == (batch_size, 5, 32)
         assert attention_weights.shape[0] == batch_size
+
+
+def test_multi_head_attention_dropout_shapes() -> None:
+    attention = MultiHeadSelfAttentionNetwork(heads_count=2, embedding_size=32, dropout_rate=0.5)
+    input_tensor = Tensor(rng.standard_normal((2, 6, 32)))
+    output, attention_weights = attention.forward(input_tensor)
+    assert output.shape == (2, 6, 32)
+    assert attention_weights.shape[:2] == (2, 2)
applications/models/src/models/get_alpaca_equity_bars.py (2)

21-27: Use requests params rather than manual query construction

requests will handle encoding and edge cases for you; also use headers per-call to ensure validation is performed.

-    response = requests.get(url, headers=headers, timeout=30)
+    response = requests.get(url, headers=_alpaca_headers(), timeout=30)

80-85: Prefer UTC internally; only localize if needed for logging

Using NY time to compute a 6-year window is okay, but it can lead to the 'Z' bug above and DST edge conditions. Consider computing in UTC and converting only when necessary for human-oriented reporting.

-    end = datetime.now(tz=ZoneInfo("America/New_York"))
+    end = datetime.now(tz=ZoneInfo("UTC"))
     start = end - timedelta(days=365 * 6)
libraries/python/src/internal/lstm_network.py (1)

32-38: Add basic input validation for forward()

Forward assumes a 3D input (batch, time, features). Add a quick shape check and explicit int cast for sequence_length for clarity.

     def forward(
         self,
-        inputs: Tensor,
+        inputs: Tensor,
         state: tuple[list[Tensor], list[Tensor]] | None = None,
     ) -> tuple[Tensor, tuple[list[Tensor], list[Tensor]]]:
-        batch_size, sequence_length, _ = inputs.shape
+        if len(inputs.shape) != 3:
+            raise ValueError(f"Expected inputs with shape (batch, time, features), got {inputs.shape}")
+        batch_size, sequence_length, _ = inputs.shape
+        sequence_length = int(sequence_length)

Also applies to: 39-50, 53-55

.mise.toml (2)

44-50: Make port mapping configurable; avoid hard-coding 8080

Parameterize host/container ports to avoid collisions and allow non-8080 services.

 [tasks."application:service:run:production"]
 description = "Run the service application"
 run = """
 docker run \
 --env-file .env \
- --publish 8080:8080 \
+ --publish {{arg(name="host_port", default="8080")}}:{{arg(name="container_port", default="8080")}} \
 pocketsizefund/{{arg(name="application_name")}}:latest \
 """

55-57: Path updates to applications/ look good; add a unit-tests task using uv for faster local runs

The cd path changes are correct. Given our team guideline (remembered from prior feedback) to use mise tasks for tests/format/lint, consider adding a fast unit test task that runs directly with uv/pytest (complementary to the docker-based integration harness).

Suggested addition (new task):

[tasks."python:test:unit"]
description = "Run Python unit tests locally with uv/pytest"
run = """
uv run pytest -q --disable-warnings --maxfail=1
"""

Also consider a coverage variant if desired.

Also applies to: 62-64, 69-71, 76-78

applications/datamanager/src/datamanager/main.py (3)

193-205: Keep error telemetry but avoid double-logging full tracebacks

You already log the exception message and a traceback. Consider reducing to one structured log with e and traceback once to keep logs concise, or keep as-is if you rely on message + trace separation.


208-223: CloudEvent success payload includes a date twice; clarify intent

create_cloud_event_success injects a "date" (now) and you pass "date" (request_fetch_date). The merge order makes your provided date win, which is fine, but the implicit override is non-obvious. Either:

  • remove "date" from the data payload here (let the helper set now), or
  • add a different key like "requested_date" to avoid ambiguity.
-        data={
-            "date": request_fetch_date,
-            "count": count,
-        },
+        data={
+            "requested_date": request_fetch_date,
+            "count": count,
+        },

If you do want the event data's "date" to be the requested date, consider documenting that in the helper or reversing the merge order in create_cloud_event_success.

Also applies to: 255-262


294-333: New /calendar endpoint: consider basic validation and rate limiting

Endpoint looks good. Consider:

  • Validating start_date <= end_date and max range to avoid heavy upstream calls.
  • Returning an empty list (200) instead of 404 is sometimes preferable for “no results” endpoints; verify with API consumers.

Would you like me to add unit tests for this endpoint (success, empty, provider error) mirroring the equity bars suite?

applications/datamanager/tests/test_datamanager_main.py (3)

92-114: Good coverage for /equity-bars database error and empty-data paths

Mocks and assertions match endpoint behavior. Consider asserting media_type and headers on success paths in a separate test for stronger contract coverage.

Also applies to: 115-138


156-173: DELETE /equity-bars tests: success and not-found cases are solid

The mocks and expectations reflect the refactored Date model. If you adopt the 500-on-exception change, add a test to assert 500 for an S3 listing error.

Also applies to: 174-193


196-207: POST /equity-bars/fetch tests look good; consider adding /calendar tests

Add tests for:

  • GET /calendar success: mock AlpacaClient.get_calendar to return data; assert 200 and JSON structure.
  • GET /calendar empty: return []; assert 404 (or 200 if you change semantics).
  • GET /calendar provider error: raise Exception; assert 500.

I can draft these test cases if you’d like.

Also applies to: 232-261

libraries/python/src/internal/summaries.py (1)

4-6: BarsSummary model is fine; consider tightening validation and export surface

Current implementation matches the tests (string date, int count). If you want to harden the model without changing semantics:

  • Forbid unexpected fields.
  • Optionally validate date strings follow YYYY-MM-DD (keeps current usage intact if consumers already use this format).
-from pydantic import BaseModel
+from pydantic import BaseModel, ConfigDict, field_validator
+import re
@@
 class BarsSummary(BaseModel):
-    date: str
-    count: int
+    model_config = ConfigDict(extra="forbid")
+
+    date: str
+    count: int
+
+    @field_validator("date")
+    def _validate_date_format(cls, v: str) -> str:  # noqa: N805
+        # Accept strictly YYYY-MM-DD; relax if broader formats are needed
+        if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", v):
+            raise ValueError("date must match YYYY-MM-DD")
+        return v

If you prefer not to enforce the date format yet, at least add extra="forbid" to prevent accidental fields from being accepted.

applications/positionmanager/tests/test_positionmanager_main.py (2)

46-59: Mocks for Arrow/Polars/Optimizer look correct; consider autospec for interface safety

The patch targets and order look right. To reduce brittle tests and catch interface drift, use autospec=True (where possible) and assert calls on these mocks to ensure your code exercised the expected paths.


71-87: Strengthen assertions to ensure side effects (order placement) occur

After the POST succeeds, assert that the optimizer and Alpaca client were actually used with expected arguments.

@@
             response = client.post("/positions/open", json=cloud_event_data)
 
         assert response.status_code == status.HTTP_200_OK
         response_data = response.json()
         assert "source" in response_data
         assert "type" in response_data
+        # Strengthen: verify business interactions occurred
+        MockPortfolioOptimizer.assert_called_once()
+        mock_optimizer_instance.get_optimized_portfolio.assert_called_once()
+        # Ensure we attempted to place orders using the computed allocation
+        assert mock_alpaca_instance.place_notional_order.call_count >= 1
libraries/python/tests/test_dataset.py (2)

49-79: Consider asserting expected dimension counts, not just presence

You can make this test more robust by checking counts match the current feature schema (helps detect accidental regressions in feature sets).

@@
     dimensions = dataset.get_dimensions()
 
-    assert "encoder_categorical_features" in dimensions
-    assert "encoder_continuous_features" in dimensions
-    assert "decoder_categorical_features" in dimensions
-    assert "decoder_continuous_features" in dimensions
-    assert "static_categorical_features" in dimensions
-    assert "static_continuous_features" in dimensions
+    assert dimensions["encoder_categorical_features"] > 0
+    assert dimensions["encoder_continuous_features"] > 0
+    assert dimensions["decoder_categorical_features"] > 0
+    assert dimensions["decoder_continuous_features"] == 0
+    assert dimensions["static_categorical_features"] > 0
+    assert dimensions["static_continuous_features"] == 0

81-135: LGTM: Prediction batch shape checks hit key invariants

Great coverage of encoder/decoder/static partitions and lengths.

As a follow-up, consider adding negative tests to lock error semantics:

  • invalid data_type raises ValueError,
  • insufficient days for requested input/output lengths raises ValueError,
  • missing/extra columns raises ValueError.

Example additions (outside the selected lines):

def test_get_batches_invalid_type_raises(dataframe_factory):
    dataset = TemporalFusionTransformerDataset(data=dataframe_factory(days=3))
    with pytest.raises(ValueError):
        dataset.get_batches(data_type="oops")

def test_get_batches_insufficient_days_raises():
    data = pl.DataFrame({
        "timestamp": [1672531200000],  # single day
        "open_price": [100.0],
        "high_price": [110.0],
        "low_price": [90.0],
        "close_price": [105.0],
        "volume": [1000.0],
        "volume_weighted_average_price": [105.0],
        "ticker": ["AAPL"],
        "sector": ["Technology"],
        "industry": ["Consumer Electronics"],
        "is_holiday": [False],
    })
    dataset = TemporalFusionTransformerDataset(data=data)
    with pytest.raises(ValueError):
        dataset.get_batches(data_type="predict", input_length=2, output_length=1)
applications/positionmanager/src/positionmanager/clients.py (1)

120-123: Use DateRange.to_object() to construct request params (and avoid duplication).

You already expose a stable payload shape via DateRange.to_object(). Using it here reduces duplication and centralizes the date formatting.

-        params = {
-            "start_date": date_range.start.isoformat(),
-            "end_date": date_range.end.isoformat(),
-        }
+        params = date_range.to_object()
libraries/python/tests/test_dates.py (2)

10-19: Patch datetime with wraps to avoid over-mocking the module.

Patching the entire internal.dates.datetime module can inadvertently mock attributes used elsewhere. Using wraps=datetime preserves real behavior except for what you override, making the test more robust.

-def test_date_default() -> None:
-    with patch("internal.dates.datetime") as mock_datetime:
+def test_date_default() -> None:
+    with patch("internal.dates.datetime", wraps=datetime) as mock_datetime:
         mock_datetime.datetime.now.return_value = datetime.datetime(
             2023, 5, 15, 10, 30, 0, tzinfo=ZoneInfo("America/New_York")
         )
-        mock_datetime.date = datetime.date

29-33: Align test name with behavior: pass a string when testing “string dash format”.

The current test name suggests a string input, but a date object is provided. Either rename the test or pass the string.

-def test_date_string_dash_format() -> None:
-    date_instance = Date(date=datetime.date.fromisoformat("2023-03-15"))
+def test_date_string_dash_format() -> None:
+    date_instance = Date(date="2023-03-15")
libraries/python/tests/test_loss_functions.py (2)

17-19: Tighten the scalar-shape assertion.

The current shape assertion is too permissive and could mask regressions. Assert that the numpy view is a scalar or a single-element vector.

-    assert isinstance(loss, Tensor)
-    assert len(loss.shape) == 0 or loss.shape in [(), (0,), (1,)]
+    assert isinstance(loss, Tensor)
+    assert loss.numpy().shape in [(), (1,)]

28-30: Make shape assertion consistent and strict.

Same rationale as above.

-    assert isinstance(loss, Tensor)
-    assert len(loss.shape) == 0 or loss.shape in [(), (0,), (1,)]
+    assert isinstance(loss, Tensor)
+    assert loss.numpy().shape in [(), (1,)]
libraries/python/tests/test_variable_selection_network.py (1)

56-58: Add a finiteness check to catch NaNs/Infs.

This small assertion hardens the test against numerical issues without being brittle.

     output_numpy = output.numpy()
     assert np.all(output_numpy >= 0.0)
     assert np.all(output_numpy <= 1.0)
+    assert np.isfinite(output_numpy).all()
libraries/python/tests/test_equity_bar.py (2)

133-146: Fix mismatch between test name and input under test (pass a string, not a date).

The test is named “…_timestamp_string_iso_format” but passes a date object created via date.fromisoformat. Pass the raw string to exercise the validator’s string parsing path.

Apply this diff:

-    equity_bar = EquityBar(
-        ticker="AAPL",
-        timestamp=date.fromisoformat("2023-06-15"),
-        open_price=150.0,
-        high_price=155.0,
-        low_price=149.0,
-        close_price=153.0,
-        volume=1000000,
-        volume_weighted_average_price=152.5,
-    )
+    equity_bar = EquityBar(
+        ticker="AAPL",
+        timestamp="2023-06-15",
+        open_price=150.0,
+        high_price=155.0,
+        low_price=149.0,
+        close_price=153.0,
+        volume=1000000,
+        volume_weighted_average_price=152.5,
+    )

170-185: Consider adding tests for volume and VWAP non-negativity.

Volume and VWAP should never be negative in practice. The model currently doesn’t validate these. Recommend adding tests to guard these invariants; see model-side suggestion in internal/equity_bar.py.

Would you like me to add parametrized tests for negative volume/VWAP as follow-ups?

applications/datamanager/tests/test_datamanager_models.py (2)

20-23: Be explicit with zero-padded dates to avoid locale/implementation quirks.

While Python’s strptime usually accepts “2023-5-15” for "%Y-%m-%d", using “2023-05-15” ensures strict adherence to the documented format and avoids edge-case failures.

Apply this diff:

-        date_with_string = Date(date="2023-5-15")  # type: ignore
+        date_with_string = Date(date="2023-05-15")  # type: ignore

85-90: Confirm domain semantics for negative bar counts.

The test asserts negative counts are acceptable. If BarsSummary.count is intended to be non-negative (typical for aggregate counts), consider adding validation to forbid negatives. If negatives are used for sentinel/error values, keep as-is and document.

libraries/python/src/internal/equity_bar.py (1)

40-49: Optional: accept datetime inputs in timestamp validator.

If upstream may pass datetime (not just date or ISO strings), extend the validator to accept datetime and call .date().

Proposed snippet (outside selected range):

from datetime import date, datetime

@field_validator("timestamp", mode="before")
@classmethod
def validate_timestamp(cls, value: date | datetime | str) -> date:
    if isinstance(value, date) and not isinstance(value, datetime):
        return value
    if isinstance(value, datetime):
        return value.date()
    try:
        return date.fromisoformat(value)
    except ValueError as e:
        raise ValueError("Invalid date format: expected YYYY-MM-DD") from e
applications/models/src/models/train_tft_model.py (3)

32-43: Use total_seconds() for accurate read timing.

.seconds drops minutes/hours and ignores subseconds. Prefer total_seconds().

Apply this diff:

-    runtime_seconds = (datetime.now(tz=timezone) - start_time).seconds
+    runtime_seconds = int((datetime.now(tz=timezone) - start_time).total_seconds())

111-126: Use total_seconds() for accurate validation timing.

Apply this diff:

-    runtime_seconds = (datetime.now(tz=timezone) - start_time).seconds
+    runtime_seconds = int((datetime.now(tz=timezone) - start_time).total_seconds())

130-139: Use total_seconds() for accurate save timing.

Apply this diff:

-    runtime_seconds = (datetime.now(tz=timezone) - start_time).seconds
+    runtime_seconds = int((datetime.now(tz=timezone) - start_time).total_seconds())
libraries/python/src/internal/dataset.py (2)

10-26: Add type hints for Scaler attributes

The Scaler class uses self.means and self.standard_deviations without type declarations, which could lead to typing issues and makes the code less self-documenting.

 class Scaler:
     def __init__(self) -> None:
-        pass
+        self.means: pl.DataFrame | None = None
+        self.standard_deviations: pl.DataFrame | None = None

     def fit(self, data: pl.DataFrame) -> None:

108-126: Consider extracting the magic number for Friday

The Friday number (4) is hardcoded and lacks context. Consider using a named constant for clarity.

-        friday_number = 4
+        FRIDAY_WEEKDAY_INDEX = 4  # Monday=0, ..., Friday=4
 
         # set is_holiday value for missing weekdays
         data = (
             data.with_columns(
                 pl.col("datetime").dt.weekday().alias("temporary_weekday")
             )
             .with_columns(
                 pl.when(
                     pl.col("is_holiday").is_null()
-                    & (pl.col("temporary_weekday") <= friday_number)
+                    & (pl.col("temporary_weekday") <= FRIDAY_WEEKDAY_INDEX)
                 )
                 .then(True)  # noqa: FBT003
                 .when(
                     pl.col("is_holiday").is_null()
-                    & (pl.col("temporary_weekday") > friday_number)
+                    & (pl.col("temporary_weekday") > FRIDAY_WEEKDAY_INDEX)
                 )
applications/positionmanager/src/positionmanager/main.py (1)

168-173: Consider extracting the type check to avoid duplication

The event data validation logic appears multiple times. Consider creating a helper function to validate event data format.

Create a helper function at the module level:

def validate_event_data(event_data: Any) -> dict:
    """Validate that event data is a dictionary."""
    if not isinstance(event_data, dict):
        raise ValueError("Invalid event data format, expected a dictionary.")
    return event_data

Then use it in the handler:

     try:
-        event_data = event.data
-        if not isinstance(event_data, dict):
-            return create_cloud_event_error(
-                application_name="positionmanager",
-                event_metadata=["positions", "open"],
-                error_message="Invalid event data format, expected a dictionary.",
-            )
+        event_data = validate_event_data(event.data)

Comment on lines +27 to +33
class AlpacaClient:
def __init__(self, alpaca_api_key_id: str, alpaca_api_secret: str) -> None:
self.trading_client = TradingClient(
api_key=alpaca_api_key_id,
secret_key=alpaca_api_secret,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Initialize TradingClient with explicit paper mode and raw_data=False to match attribute access.

By default, alpaca-py may return raw dicts (raw_data=True), which would break attribute access (calendar_entry.date/open/close). Also, explicitly enabling paper mode avoids accidental live trading.

Apply:

 class AlpacaClient:
     def __init__(self, alpaca_api_key_id: str, alpaca_api_secret: str) -> None:
-        self.trading_client = TradingClient(
-            api_key=alpaca_api_key_id,
-            secret_key=alpaca_api_secret,
-        )
+        self.trading_client = TradingClient(
+            api_key=alpaca_api_key_id,
+            secret_key=alpaca_api_secret,
+            paper=True,
+            raw_data=False,
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class AlpacaClient:
def __init__(self, alpaca_api_key_id: str, alpaca_api_secret: str) -> None:
self.trading_client = TradingClient(
api_key=alpaca_api_key_id,
secret_key=alpaca_api_secret,
)
class AlpacaClient:
def __init__(self, alpaca_api_key_id: str, alpaca_api_secret: str) -> None:
self.trading_client = TradingClient(
api_key=alpaca_api_key_id,
secret_key=alpaca_api_secret,
paper=True,
raw_data=False,
)
🤖 Prompt for AI Agents
In applications/datamanager/src/datamanager/clients.py around lines 27 to 33,
the TradingClient is constructed without explicit paper mode and raw_data
settings which can cause returned objects to be raw dicts (breaking attribute
access) and risk accidental live trading; update the TradingClient
initialization to set paper=True and raw_data=False so the client uses paper
trading and returns model objects with attribute access.

Comment on lines +40 to +53
"list[Calendar]", self.trading_client.get_calendar(request)
)

calendar_list = []
for calendar_entry in calendar_data:
calendar_dict = {
"date": calendar_entry.date,
"open": calendar_entry.open,
"close": calendar_entry.close,
}
calendar_list.append(calendar_dict)

return calendar_list

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure get_calendar returns strings as annotated and is robust to raw_data mode.

The return type is list[dict[str, str]], but date/open/close may be date/time objects (or dict values if raw_data=True). Normalize to strings.

Apply:

-        calendar_list = []
-        for calendar_entry in calendar_data:
-            calendar_dict = {
-                "date": calendar_entry.date,
-                "open": calendar_entry.open,
-                "close": calendar_entry.close,
-            }
-            calendar_list.append(calendar_dict)
+        calendar_list: list[dict[str, str]] = []
+        for entry in calendar_data:
+            # Support both model objects and raw dicts
+            if isinstance(entry, dict):
+                date_val = entry.get("date")
+                open_val = entry.get("open")
+                close_val = entry.get("close")
+            else:
+                date_val = getattr(entry, "date", None)
+                open_val = getattr(entry, "open", None)
+                close_val = getattr(entry, "close", None)
+
+            calendar_list.append(
+                {
+                    "date": str(date_val) if date_val is not None else "",
+                    "open": str(open_val) if open_val is not None else "",
+                    "close": str(close_val) if close_val is not None else "",
+                }
+            )

Given there’s another AlpacaClient in applications/positionmanager with overlapping concerns, consider centralizing an Alpaca client wrapper in libraries/python/src/internal to avoid duplication and keep config (paper/raw_data) consistent across services.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
calendar_data: list[Calendar] = cast(
"list[Calendar]", self.trading_client.get_calendar(request)
)
calendar_list = []
for calendar_entry in calendar_data:
calendar_dict = {
"date": calendar_entry.date,
"open": calendar_entry.open,
"close": calendar_entry.close,
}
calendar_list.append(calendar_dict)
return calendar_list
calendar_data: list[Calendar] = cast(
"list[Calendar]", self.trading_client.get_calendar(request)
)
calendar_list: list[dict[str, str]] = []
for entry in calendar_data:
# Support both model objects and raw dicts
if isinstance(entry, dict):
date_val = entry.get("date")
open_val = entry.get("open")
close_val = entry.get("close")
else:
date_val = getattr(entry, "date", None)
open_val = getattr(entry, "open", None)
close_val = getattr(entry, "close", None)
calendar_list.append(
{
"date": str(date_val) if date_val is not None else "",
"open": str(open_val) if open_val is not None else "",
"close": str(close_val) if close_val is not None else "",
}
)
return calendar_list

Comment on lines +11 to +19
end = datetime.now(tz=ZoneInfo("America/New_York"))
start = end - timedelta(days=365 * 6)

base_url = "https://api.alpaca.markets/v2/calendar"
params = {
"start": f"{start.isoformat()}Z",
"end": f"{end.isoformat()}Z",
}
url = f"{base_url}?{urllib.parse.urlencode(params)}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect use of 'Z' with America/New_York timestamps; send date-only parameters.

You're appending 'Z' (UTC designator) to New York local datetimes, producing invalid timestamps (e.g., ...-04:00Z). The Alpaca Calendar API expects YYYY-MM-DD dates. Use date-only parameters to avoid timezone issues.

Apply this diff:

-    end = datetime.now(tz=ZoneInfo("America/New_York"))
-    start = end - timedelta(days=365 * 6)
+    end = datetime.now(tz=ZoneInfo("America/New_York"))
+    start = end - timedelta(days=365 * 6)
@@
-    params = {
-        "start": f"{start.isoformat()}Z",
-        "end": f"{end.isoformat()}Z",
-    }
+    params = {
+        "start": start.date().isoformat(),
+        "end": end.date().isoformat(),
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
end = datetime.now(tz=ZoneInfo("America/New_York"))
start = end - timedelta(days=365 * 6)
base_url = "https://api.alpaca.markets/v2/calendar"
params = {
"start": f"{start.isoformat()}Z",
"end": f"{end.isoformat()}Z",
}
url = f"{base_url}?{urllib.parse.urlencode(params)}"
end = datetime.now(tz=ZoneInfo("America/New_York"))
start = end - timedelta(days=365 * 6)
base_url = "https://api.alpaca.markets/v2/calendar"
params = {
"start": start.date().isoformat(),
"end": end.date().isoformat(),
}
url = f"{base_url}?{urllib.parse.urlencode(params)}"
🤖 Prompt for AI Agents
In applications/models/src/models/get_alpaca_calendar.py around lines 11 to 19,
the code currently appends a 'Z' to America/New_York datetimes which yields
invalid mixed timezone strings; change the parameters to date-only strings in
YYYY-MM-DD format (use start.date().isoformat() and end.date().isoformat()),
remove the trailing 'Z', and build the query params with those date strings so
the Alpaca Calendar API receives start and end as date-only values.

Comment on lines +21 to +25
headers = {
"accept": "application/json",
"APCA-API-KEY-ID": os.getenv("APCA_API_KEY_ID"),
"APCA-API-SECRET-KEY": os.getenv("APCA_API_SECRET_KEY"),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate required Alpaca API credentials before making the request.

If env vars are missing, the request will 401 without a clear error. Fail fast with a descriptive message. Also consider a User-Agent for observability.

-    headers = {
-        "accept": "application/json",
-        "APCA-API-KEY-ID": os.getenv("APCA_API_KEY_ID"),
-        "APCA-API-SECRET-KEY": os.getenv("APCA_API_SECRET_KEY"),
-    }
+    api_key_id = os.getenv("APCA_API_KEY_ID") or os.getenv("ALPACA_API_KEY_ID")
+    api_secret_key = os.getenv("APCA_API_SECRET_KEY") or os.getenv("ALPACA_API_SECRET_KEY")
+    if not api_key_id or not api_secret_key:
+        raise RuntimeError(
+            "Missing Alpaca API credentials. Set APCA_API_KEY_ID/APCA_API_SECRET_KEY "
+            "or ALPACA_API_KEY_ID/ALPACA_API_SECRET_KEY."
+        )
+
+    headers = {
+        "accept": "application/json",
+        "APCA-API-KEY-ID": api_key_id,
+        "APCA-API-SECRET-KEY": api_secret_key,
+        "User-Agent": "pocketsizefund-models/1.0 (+https://github.com/pocketsizefund/pocketsizefund)",
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
headers = {
"accept": "application/json",
"APCA-API-KEY-ID": os.getenv("APCA_API_KEY_ID"),
"APCA-API-SECRET-KEY": os.getenv("APCA_API_SECRET_KEY"),
}
api_key_id = os.getenv("APCA_API_KEY_ID") or os.getenv("ALPACA_API_KEY_ID")
api_secret_key = os.getenv("APCA_API_SECRET_KEY") or os.getenv("ALPACA_API_SECRET_KEY")
if not api_key_id or not api_secret_key:
raise RuntimeError(
"Missing Alpaca API credentials. Set APCA_API_KEY_ID/APCA_API_SECRET_KEY "
"or ALPACA_API_KEY_ID/ALPACA_API_SECRET_KEY."
)
headers = {
"accept": "application/json",
"APCA-API-KEY-ID": api_key_id,
"APCA-API-SECRET-KEY": api_secret_key,
"User-Agent": "pocketsizefund-models/1.0 (+https://github.com/pocketsizefund/pocketsizefund)",
}
🤖 Prompt for AI Agents
In applications/models/src/models/get_alpaca_calendar.py around lines 21 to 25,
the code reads Alpaca credentials directly from env vars into headers without
validating them; add an explicit check that APCA_API_KEY_ID and
APCA_API_SECRET_KEY are present (fail fast by raising a clear exception or
logging an error and exiting) before constructing the request headers, and
include a User-Agent header (e.g., app name/version) for observability so
requests fail with a descriptive message instead of a generic 401.

💡 Verification agent

🧩 Analysis chain

Confirm environment variable naming across the repo (APCA_ vs ALPACA_).

This script reads APCA_; other components may use ALPACA_ variants after recent refactors. Ensure consistency to avoid prod misconfig.

Run this to audit env-var usage:


🏁 Script executed:

#!/bin/bash
# Find all Alpaca env-var names
rg -n --pretty $'APCA_API_KEY_ID|APCA_API_SECRET_KEY|ALPACA_API_KEY_ID|ALPACA_API_SECRET_KEY|alpaca_api_key_id|alpaca_api_secret_key'

Length of output: 1120


Use ALPACA_ env vars consistently — update get_alpaca_calendar.py*

The repo standard is ALPACA_API_KEY_ID / ALPACA_API_SECRET_KEY; only get_alpaca_calendar.py is using APCA_* env var names and should be updated. HTTP header names ("APCA-API-KEY-ID" / "APCA-API-SECRET-KEY") are correct and should remain unchanged.

Files to change / verify:

  • applications/models/src/models/get_alpaca_calendar.py — lines ~23-24: replace APCA_* env var names with ALPACA_*.
  • Evidence of ALPACA_* usage elsewhere:
    • applications/models/src/models/get_alpaca_equity_bars.py:13 (os.getenv("ALPACA_API_KEY_ID"))
    • applications/datamanager/src/datamanager/main.py:60 (os.getenv("ALPACA_API_KEY_ID", ""))
    • applications/positionmanager/src/positionmanager/main.py:66,124,262 (os.getenv("ALPACA_API_KEY_ID", ""))
    • infrastructure/environment_variables.py:8,22 (require_secret / mapping for "ALPACA_API_KEY_ID")

Suggested change (in get_alpaca_calendar.py):
Replace:
"APCA-API-KEY-ID": os.getenv("APCA_API_KEY_ID"),
"APCA-API-SECRET-KEY": os.getenv("APCA_API_SECRET_KEY"),
With:
"APCA-API-KEY-ID": os.getenv("ALPACA_API_KEY_ID"),
"APCA-API-SECRET-KEY": os.getenv("ALPACA_API_SECRET_KEY"),

Also verify CI/infra secrets and any deployment configs use ALPACA_* (or update them accordingly) to avoid runtime breakage.

🤖 Prompt for AI Agents
In applications/models/src/models/get_alpaca_calendar.py around lines 21 to 25
the code is reading environment variables using the APCA_* names which are
inconsistent with the repo standard; change the env var keys used in os.getenv
to ALPACA_API_KEY_ID and ALPACA_API_SECRET_KEY (leave the HTTP header names
"APCA-API-KEY-ID" and "APCA-API-SECRET-KEY" unchanged) so the headers are
populated from os.getenv("ALPACA_API_KEY_ID") and
os.getenv("ALPACA_API_SECRET_KEY"); after making the change, verify CI/infra
secrets and deployment configs use ALPACA_* names to avoid runtime breakage.

Comment on lines +27 to +45
response = requests.get(url, headers=headers, timeout=10)

response.raise_for_status()

body = response.json()

calendar_data = pl.DataFrame(body)

calendar_data.write_csv("calendar.csv")

logger.info("Calendar data has been written to calendar.csv")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Wrap network call and JSON parsing with error handling and logging.

Surface actionable errors and preserve stack traces. Keep success path unchanged.

-    response = requests.get(url, headers=headers, timeout=10)
-
-    response.raise_for_status()
-
-    body = response.json()
-
-    calendar_data = pl.DataFrame(body)
-
-    calendar_data.write_csv("calendar.csv")
-
-    logger.info("Calendar data has been written to calendar.csv")
+    try:
+        response = requests.get(url, headers=headers, timeout=10)
+        response.raise_for_status()
+        body = response.json()
+    except requests.RequestException as exc:
+        logger.exception(f"Failed to fetch Alpaca calendar: {exc}")
+        raise
+
+    calendar_data = pl.DataFrame(body)
+    calendar_data.write_csv("calendar.csv")
+    logger.info("Calendar data has been written to calendar.csv")

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In applications/models/src/models/get_alpaca_calendar.py around lines 27 to 37,
the network request and JSON parsing are unprotected; wrap the requests.get
call, response.raise_for_status, and response.json() in a try/except that
catches request and JSON errors (requests.exceptions.RequestException,
ValueError/JSONDecodeError, plus a general Exception fallback), log the full
exception with stack trace (using logger.exception or logger.error with
exc_info=True) and surface an actionable error (re-raise or return an error
result) while leaving the existing success path (creating the DataFrame, writing
CSV, and the info log) unchanged.

Comment on lines +191 to +225
def train(
self,
inputs_list: list[dict[str, Tensor]],
epoch_count: int,
learning_rate: float = 1e-3,
) -> dict[str, list[float]]:
optimizer = Adam(params=self.parameters, lr=learning_rate)

losses: list[float] = []

for _ in range(epoch_count):
epoch_loss: float = 0.0

for inputs in inputs_list:
outputs = self.forward(inputs)

loss = quantile_loss(
outputs["predictions"],
inputs["targets"],
self.quantiles,
)

optimizer.zero_grad()
_ = loss.backward()
optimizer.step()

epoch_loss += loss.numpy().item()

average_epoch_loss: float = epoch_loss / len(inputs_list)
losses.append(average_epoch_loss)

return {
"losses": losses,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use quantile outputs for loss and fold time into batch to match loss expectations.

quantile_loss expects predictions with a quantile dimension and targets without it. Currently you pass predictions (no quantile dim). Flatten the time axis and use outputs["quantiles"].

Apply this diff:

-        losses: list[float] = []
+        losses: list[float] = []
@@
-            for inputs in inputs_list:
-                outputs = self.forward(inputs)
-
-                loss = quantile_loss(
-                    outputs["predictions"],
-                    inputs["targets"],
-                    self.quantiles,
-                )
+            for inputs in inputs_list:
+                outputs = self.forward(inputs)
+                # reshape to (B*T_out, output_size, Q) and (B*T_out, output_size)
+                q = outputs["quantiles"]
+                t = inputs["targets"]
+                bt = q.shape[0] * q.shape[1]
+                q2 = q.reshape(bt, self.output_size, len(self.quantiles))
+                t2 = t.reshape(bt, self.output_size)
+                loss = quantile_loss(q2, t2, self.quantiles)
@@
-                epoch_loss += loss.numpy().item()
+                epoch_loss += float(loss.numpy().item())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def train(
self,
inputs_list: list[dict[str, Tensor]],
epoch_count: int,
learning_rate: float = 1e-3,
) -> dict[str, list[float]]:
optimizer = Adam(params=self.parameters, lr=learning_rate)
losses: list[float] = []
for _ in range(epoch_count):
epoch_loss: float = 0.0
for inputs in inputs_list:
outputs = self.forward(inputs)
loss = quantile_loss(
outputs["predictions"],
inputs["targets"],
self.quantiles,
)
optimizer.zero_grad()
_ = loss.backward()
optimizer.step()
epoch_loss += loss.numpy().item()
average_epoch_loss: float = epoch_loss / len(inputs_list)
losses.append(average_epoch_loss)
return {
"losses": losses,
}
def train(
self,
inputs_list: list[dict[str, Tensor]],
epoch_count: int,
learning_rate: float = 1e-3,
) -> dict[str, list[float]]:
optimizer = Adam(params=self.parameters, lr=learning_rate)
losses: list[float] = []
for _ in range(epoch_count):
epoch_loss: float = 0.0
for inputs in inputs_list:
outputs = self.forward(inputs)
# reshape to (B*T_out, output_size, Q) and (B*T_out, output_size)
q = outputs["quantiles"]
t = inputs["targets"]
bt = q.shape[0] * q.shape[1]
q2 = q.reshape(bt, self.output_size, len(self.quantiles))
t2 = t.reshape(bt, self.output_size)
loss = quantile_loss(q2, t2, self.quantiles)
optimizer.zero_grad()
_ = loss.backward()
optimizer.step()
epoch_loss += float(loss.numpy().item())
average_epoch_loss: float = epoch_loss / len(inputs_list)
losses.append(average_epoch_loss)
return {
"losses": losses,
}
🤖 Prompt for AI Agents
In libraries/python/src/internal/tft_model.py around lines 191 to 224, the
training loop passes outputs["predictions"] (no quantile dimension) to
quantile_loss and does not fold the time axis into the batch, causing a shape
mismatch with quantile_loss which expects predictions with a quantile dim and
targets without it; update the code to use outputs["quantiles"] instead of
outputs["predictions"] and reshape both predictions and targets by merging batch
and time dimensions before calling quantile_loss (e.g., reshape quantile
predictions to (batch*time, num_quantiles) and targets to (batch*time,) or
(batch*time, 1) as required), then compute loss, backpropagate, and continue as
before.

Comment on lines +226 to +247
def validate(
self,
inputs_list: list[dict[str, Tensor]],
) -> float:
total_loss = 0.0
batch_count = 0

for inputs in inputs_list:
outputs = self.forward(inputs)

loss = quantile_loss(
outputs["predictions"],
inputs["targets"],
self.quantiles,
)

total_loss += loss.numpy().item()
batch_count += 1

return total_loss / batch_count

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validation path mirrors training bug; use quantiles and fold time.

Same shape issue as train(). Fix accordingly.

Apply this diff:

-        for inputs in inputs_list:
-            outputs = self.forward(inputs)
-
-            loss = quantile_loss(
-                outputs["predictions"],
-                inputs["targets"],
-                self.quantiles,
-            )
-
-            total_loss += loss.numpy().item()
+        for inputs in inputs_list:
+            outputs = self.forward(inputs)
+            q = outputs["quantiles"]
+            t = inputs["targets"]
+            bt = q.shape[0] * q.shape[1]
+            q2 = q.reshape(bt, self.output_size, len(self.quantiles))
+            t2 = t.reshape(bt, self.output_size)
+            loss = quantile_loss(q2, t2, self.quantiles)
+            total_loss += float(loss.numpy().item())

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In libraries/python/src/internal/tft_model.py around lines 226 to 246, the
validate() method currently mirrors the training bug by passing mismatched
shapes to quantile_loss; fix it by using the model's quantiles and folding the
time dimension the same way train() does: for each batch slice or inputs,
select/slice the forecast time fold from outputs["predictions"] so its time axis
matches inputs["targets"], reshape or expand inputs["targets"] to include the
quantile axis (or transpose predictions to [batch, time, quantile]) and then
call quantile_loss with self.quantiles so shapes align before accumulating loss
and averaging over batches.

Comment on lines +5 to +16
class VariableSelectionNetwork:
def __init__(self, input_dimension: int, hidden_size: int) -> None:
self.input_layer = nn.Linear(
in_features=input_dimension,
out_features=hidden_size,
)

self.output_layer = nn.Linear(
in_features=hidden_size,
out_features=input_dimension,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Store input_dimension and validate shapes early

Capturing the declared input dimension and validating inputs at runtime helps catch silent shape mismatches, especially when integrating into TFT where concatenation dims can drift.

 class VariableSelectionNetwork:
     def __init__(self, input_dimension: int, hidden_size: int) -> None:
-        self.input_layer = nn.Linear(
+        self.input_dimension = input_dimension
+        self.hidden_size = hidden_size
+
+        self.input_layer = Linear(
             in_features=input_dimension,
             out_features=hidden_size,
         )
 
-        self.output_layer = nn.Linear(
+        self.output_layer = Linear(
             in_features=hidden_size,
             out_features=input_dimension,
         )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In libraries/python/src/internal/variable_selection_network.py around lines 5 to
16, the class constructor does not persist the declared input_dimension nor
validate incoming tensor shapes; update __init__ to store self.input_dimension =
input_dimension (and optionally self.hidden_size = hidden_size) and then in the
forward method validate that the input tensor's last dimension equals
self.input_dimension, raising a clear ValueError if it does not so shape
mismatches are caught early; ensure any other internal layers that depend on
input_dimension use the stored attribute for consistency.

@@ -1,19 +1,16 @@
import numpy as np
import pytest
from internal.loss_functions import quantile_loss
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding validation in quantile_loss for predictions vs quantiles size (and fix accumulation).

The internal implementation would benefit from:

  • Validating predictions.shape[-1] == len(quantiles) with a clear error.
  • Accumulating loss correctly instead of ignoring add’s return value.

Proposed change in libraries/python/src/internal/loss_functions.py (outside this diff):

 def quantile_loss(
     predictions: Tensor,  # shape: (batch_size, output_size, len(quantiles))
     targets: Tensor,  # shape: (batch_size, output_size)
     quantiles: list[float] | None = None,
 ) -> Tensor:
     if quantiles is None:
         quantiles = [0.1, 0.5, 0.9]
 
     if not all(0 <= q <= 1 for q in quantiles):
         message = "All quantiles must be between 0 and 1"
         raise ValueError(message)
 
+    if predictions.shape[-1] != len(quantiles):
+        message = (
+            f"Predictions last dimension ({predictions.shape[-1]}) must equal "
+            f"number of quantiles ({len(quantiles)})"
+        )
+        raise ValueError(message)
+
-    errors_total = Tensor.zeros((1,))
+    errors_total = Tensor.zeros((1,))
     for index, quantile in enumerate(quantiles):
         error = targets.sub(predictions[:, :, index])
         quantile_tensor = Tensor([quantile])
-        errors_total.add(
+        errors_total = errors_total.add(
             Tensor.where(
                 error > 0,
                 quantile_tensor.mul(error),
                 (quantile_tensor.sub(1)).mul(error),
             ).mean()
         )
 
     return errors_total.div(len(quantiles))  # shape: (1,)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from internal.loss_functions import quantile_loss
def quantile_loss(
predictions: Tensor, # shape: (batch_size, output_size, len(quantiles))
targets: Tensor, # shape: (batch_size, output_size)
quantiles: list[float] | None = None,
) -> Tensor:
if quantiles is None:
quantiles = [0.1, 0.5, 0.9]
if not all(0 <= q <= 1 for q in quantiles):
message = "All quantiles must be between 0 and 1"
raise ValueError(message)
if predictions.shape[-1] != len(quantiles):
message = (
f"Predictions last dimension ({predictions.shape[-1]}) must equal "
f"number of quantiles ({len(quantiles)})"
)
raise ValueError(message)
errors_total = Tensor.zeros((1,))
for index, quantile in enumerate(quantiles):
error = targets.sub(predictions[:, :, index])
quantile_tensor = Tensor([quantile])
errors_total = errors_total.add(
Tensor.where(
error > 0,
quantile_tensor.mul(error),
(quantile_tensor.sub(1)).mul(error),
).mean()
)
return errors_total.div(len(quantiles)) # shape: (1,)
🤖 Prompt for AI Agents
In libraries/python/src/internal/loss_functions.py around the quantile_loss
function (update the implementation referenced by tests in
libraries/python/tests/test_loss_functions.py line 3), validate that
predictions.shape[-1] == len(quantiles) and raise a ValueError with a clear
message if not, and fix the accumulation by using the return value from add
(e.g., loss = loss + add(... ) or loss += add(...)) so each quantile
contribution is actually accumulated into the total loss.

Comment thread pyproject.toml
@@ -3,39 +3,19 @@ name = "pocketsizefund"
version = "20250602.4"
description = "Open source quantitative hedge fund 🍊"
requires-python = "==3.12.10"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid pinning requires-python to a patch release

Pinning to "==3.12.10" is brittle and can make local/dev CI setups unnecessarily hard. Prefer a compatible range (e.g., >=3.12,<3.13) unless you truly require a specific patch.

-requires-python = "==3.12.10"
+requires-python = ">=3.12,<3.13"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
requires-python = "==3.12.10"
requires-python = ">=3.12,<3.13"
🤖 Prompt for AI Agents
In pyproject.toml around line 5, the requires-python field is pinned to a
specific patch ("==3.12.10"), which is brittle; change it to a compatible range
such as ">=3.12,<3.13" (or another appropriate minor-range) to allow any 3.12.x
interpreter while preventing upgrades to 3.13, updating the requires-python
value accordingly in the file.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🔭 Outside diff range comments (1)
libraries/python/src/internal/lstm_network.py (1)

68-73: Dropout applied incorrectly and toggling training on a Tensor is invalid

  • Calling train() on a Tensor is not a valid tinygrad pattern (training mode is global via Tensor.training or managed at module level).
  • Dropout should affect the input to the next layer, not mutate the retained hidden_state itself. Currently, layer_input is set to the non-dropped tensor, so dropout has no effect downstream.
-                if self.dropout_rate > 0.0 and index < self.layer_count - 1:
-                    hidden_state[index].train()
-                    hidden_state[index] = hidden_state[index].dropout(self.dropout_rate)
-
-                layer_input = layer_hidden_state
+                if self.dropout_rate > 0.0 and index < self.layer_count - 1 and Tensor.training:
+                    # Apply dropout to the inter-layer signal only (do not corrupt the recurrent state).
+                    layer_input = layer_hidden_state.dropout(self.dropout_rate)
+                else:
+                    layer_input = layer_hidden_state
♻️ Duplicate comments (5)
libraries/python/src/internal/loss_functions.py (1)

9-15: Add essential input/shape validation to prevent silent broadcasting and index errors.

Ensure non-empty quantiles and validate shapes to match (B, O, Q) vs (B, O). This prevents out-of-bounds indexing at Line 18 and subtle miscomputations due to broadcasting.

Apply:

     if quantiles is None:
         quantiles = [0.1, 0.5, 0.9]

+    if len(quantiles) == 0:
+        raise ValueError("quantiles must be non-empty")
+
     if not all(0 <= q <= 1 for q in quantiles):
         message = "All quantiles must be between 0 and 1"
         raise ValueError(message)
 
+    # Validate shapes: predictions (B, O, Q), targets (B, O)
+    if len(predictions.shape) != 3:
+        raise ValueError("predictions must be 3D: (batch_size, output_size, num_quantiles)")
+    if len(targets.shape) != 2:
+        raise ValueError("targets must be 2D: (batch_size, output_size)")
+    if predictions.shape[0] != targets.shape[0] or predictions.shape[1] != targets.shape[1]:
+        raise ValueError(f"Shape mismatch: predictions {predictions.shape[:2]} vs targets {targets.shape}")
+    if predictions.shape[2] != len(quantiles):
+        raise ValueError(
+            f"Quantile count mismatch: predictions last dim={predictions.shape[2]} vs len(quantiles)={len(quantiles)}"
+        )
libraries/python/src/internal/lstm_network.py (1)

13-16: Don’t hard-enforce layer_count >= 3; allow 1–2 (warn instead)
This breaks common LSTM configs and back-compat. Keep default=3 if desired, but only error for <1 and warn for 1–2.

-        minimum_layer_count = 3
-        if layer_count < minimum_layer_count:
-            message = f"Layer count must be at least {minimum_layer_count}"
-            raise ValueError(message)
+        if layer_count < 1:
+            raise ValueError("layer_count must be at least 1")
+        if layer_count < 3:
+            import warnings
+            warnings.warn(
+                "Using fewer than 3 LSTM layers is supported but may reduce model capacity; "
+                "defaults and some models expect 3 layers.",
+                UserWarning,
+            )
applications/datamanager/src/datamanager/clients.py (2)

47-55: Normalize calendar fields: handle None and prefer ISO format; remain robust if raw_data is dicts

Returning str(None) produces "None". Prefer "" for missing values, and .isoformat() for date/time objects. Also tolerate raw dict entries if raw_data=True slips through.

-        calendar_list: list[dict[str, str]] = []
-        for calendar_entry in calendar_data:
-            calendar_dict = {
-                "date": str(calendar_entry.date),
-                "open": str(calendar_entry.open),
-                "close": str(calendar_entry.close),
-            }
-            calendar_list.append(calendar_dict)
+        calendar_list: list[dict[str, str]] = []
+        for entry in calendar_data:
+            # Support both model objects and raw dicts
+            if isinstance(entry, dict):
+                date_val = entry.get("date")
+                open_val = entry.get("open")
+                close_val = entry.get("close")
+            else:
+                date_val = getattr(entry, "date", None)
+                open_val = getattr(entry, "open", None)
+                close_val = getattr(entry, "close", None)
+
+            def fmt(val: object) -> str:
+                return (
+                    val.isoformat()  # type: ignore[attr-defined]
+                    if hasattr(val, "isoformat")
+                    else (str(val) if val is not None else "")
+                )
+
+            calendar_list.append(
+                {
+                    "date": fmt(date_val),
+                    "open": fmt(open_val),
+                    "close": fmt(close_val),
+                }
+            )

33-36: Set paper=True and raw_data=False on TradingClient to avoid live trading and ensure model objects

To prevent accidental live trading and guarantee attribute access (vs raw dicts), initialize TradingClient with explicit paper=True and raw_data=False.

-        self.trading_client: TradingClient = TradingClient(
-            api_key=alpaca_api_key_id,
-            secret_key=alpaca_api_secret,
-        )
+        self.trading_client: TradingClient = TradingClient(
+            api_key=alpaca_api_key_id,
+            secret_key=alpaca_api_secret,
+            paper=True,
+            raw_data=False,
+        )
applications/datamanager/src/datamanager/main.py (1)

273-284: Fix broken exception block (syntax error) and return consistent 500 response

There’s a stray logger call inside a Response constructor and two competing returns. CI failures cite unclosed parenthesis and invalid argument order. Replace the block with a single, coherent 500 response.

     try:
         blobs = list(s3_client.list_objects(prefix=prefix))
-    except Exception as e:  # noqa: BLE001
-        logger.error(f"Error listing S3 objects: {e}")
-        return Response(
-            status_code=status.HTTP_404_NOT_FOUND,
-        logger.error(f"Error listing equity bars: {e}")
-        return Response(
-            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
-            content=json.dumps({"error": f"Failed to list equity bars: {e!s}"}),
-            media_type="application/json",
-        )
+    except Exception as e:  # noqa: BLE001
+        logger.error(f"Error listing equity bars: {e}")
+        return Response(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            content=json.dumps({"error": f"Failed to list equity bars: {e!s}"}),
+            media_type="application/json",
+        )
🧹 Nitpick comments (4)
libraries/python/src/internal/loss_functions.py (3)

1-8: Generalize type annotation to accept tuples and other sequences.

Many callers use tuples; widen the annotation to Sequence[float] to avoid type friction.

Apply:

-from tinygrad.tensor import Tensor
+from typing import Sequence
+from tinygrad.tensor import Tensor
@@
-    quantiles: list[float] | None = None,
+    quantiles: Sequence[float] | None = None,

16-28: Optional: vectorize across quantiles to reduce Python overhead and Tensor allocations.

Removes the Python loop and constructs a broadcasted quantile tensor. Semantics preserve the current “mean across batch/output and quantiles.” If you revert to sum semantics, multiply the final mean by len(quantiles).

Apply:

-    errors_total = Tensor.zeros((1,))
-    for index, quantile in enumerate(quantiles):
-        error = targets.sub(predictions[:, :, index])
-        quantile_tensor = Tensor([quantile])
-        errors_total = errors_total.add(
-            Tensor.where(
-                error > 0,
-                quantile_tensor.mul(error),
-                (quantile_tensor.sub(1)).mul(error),
-            ).mean()
-        )
-
-    return errors_total.div(len(quantiles))  # shape: (1,)
+    # Vectorized over quantiles: predictions (B, O, Q), targets (B, O, 1), quantiles (1, 1, Q)
+    quantiles_tensor = Tensor(quantiles).reshape(1, 1, -1)
+    error = targets.reshape(targets.shape[0], targets.shape[1], 1).sub(predictions)
+    loss_per_elem = Tensor.where(
+        error > 0,
+        quantiles_tensor.mul(error),
+        quantiles_tensor.sub(1).mul(error),
+    )
+    return loss_per_elem.mean()  # mean over batch, output, and quantiles

4-8: Nit: add a short docstring clarifying expected shapes and the loss formula.

Improves readability for new contributors.

Apply:

 def quantile_loss(
     predictions: Tensor,  # shape: (batch_size, output_size, len(quantiles))
     targets: Tensor,  # shape: (batch_size, output_size)
     quantiles: list[float] | None = None,
 ) -> Tensor:
+    """
+    Compute the multi-quantile pinball loss.
+    Expected shapes:
+      - predictions: (B, O, Q) one prediction per quantile
+      - targets:     (B, O)
+      - quantiles:   sequence of floats in [0, 1]
+    Returns a (1,) tensor containing the mean loss across batch/output and quantiles.
+    """
libraries/python/src/internal/lstm_network.py (1)

39-47: Initialize state with matching device/dtype as inputs
Creating zeros without matching inputs’ device/dtype can cause subtle bugs (mixed device/dtype). Prefer initializing with inputs’ properties (e.g., using device=inputs.device, dtype=inputs.dtype or an inputs-backed helper, if available).

Can you confirm tinygrad’s Tensor.zeros supports device/dtype kwargs in your pinned version? If unsure, I can help propose a safe helper to mirror inputs’ properties.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 17c9a34 and a94fce7.

📒 Files selected for processing (7)
  • .claude/settings.local.json (1 hunks)
  • .github/workflows/teardown_infrastructure.yaml (1 hunks)
  • applications/datamanager/src/datamanager/clients.py (2 hunks)
  • applications/datamanager/src/datamanager/main.py (5 hunks)
  • applications/models/pyproject.toml (1 hunks)
  • libraries/python/src/internal/loss_functions.py (1 hunks)
  • libraries/python/src/internal/lstm_network.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • applications/models/pyproject.toml
  • .claude/settings.local.json
  • .github/workflows/teardown_infrastructure.yaml
🧰 Additional context used
🧬 Code Graph Analysis (4)
libraries/python/src/internal/loss_functions.py (3)
application/predictionengine/src/predictionengine/loss_function.py (1)
  • quantile_loss (8-29)
application/predictionengine/tests/test_loss_function.py (7)
  • test_quantile_loss_multiple_samples (24-32)
  • test_quantile_loss_basic (13-21)
  • test_quantile_loss_shapes (56-63)
  • test_quantile_loss_different_quantiles (45-53)
  • test_quantile_loss_perfect_prediction (35-42)
  • test_quantile_loss_shape_mismatch (66-72)
  • test_quantile_loss_invalid_quantiles (75-81)
application/predictionengine/src/predictionengine/miniature_temporal_fusion_transformer.py (1)
  • validate (148-161)
libraries/python/src/internal/lstm_network.py (2)
application/predictionengine/src/predictionengine/long_short_term_memory.py (3)
  • LongShortTermMemory (5-69)
  • forward (22-69)
  • __init__ (6-20)
application/predictionengine/tests/test_long_short_term_memory.py (4)
  • test_lstm_forward (31-43)
  • test_lstm_multiple_layers (58-67)
  • test_lstm_initialization (14-28)
  • test_lstm_single_timestep (70-78)
applications/datamanager/src/datamanager/main.py (3)
libraries/python/src/internal/cloud_event.py (2)
  • create_cloud_event_error (24-38)
  • create_cloud_event_success (7-21)
libraries/python/src/internal/dates.py (1)
  • Date (8-31)
applications/datamanager/src/datamanager/clients.py (7)
  • AlpacaClient (27-27)
  • AlpacaClient (28-56)
  • PolygonClient (14-24)
  • S3Client (59-83)
  • get_all_equity_bars (18-24)
  • list_objects (65-73)
  • get_calendar (37-56)
applications/datamanager/src/datamanager/clients.py (2)
applications/datamanager/src/datamanager/main.py (1)
  • get_calendar (298-335)
application/positionmanager/src/positionmanager/clients.py (2)
  • AlpacaClient (16-103)
  • __init__ (17-33)
🪛 Ruff (0.12.2)
applications/datamanager/src/datamanager/main.py

279-279: SyntaxError: Positional argument cannot follow keyword argument


279-280: SyntaxError: Expected ')', found newline

applications/datamanager/src/datamanager/clients.py

28-28: SyntaxError: Expected an indented block after class definition

🪛 GitHub Actions: Test and coverage check
applications/datamanager/src/datamanager/main.py

[error] 277-277: SyntaxError: '(' was never closed. Likely a missing closing parenthesis in a return statement.

🪛 GitHub Actions: Code quality check
applications/datamanager/src/datamanager/main.py

[error] 279-279: Ruff format failed. Command: 'ruff format'. Parse error in applications/datamanager/src/datamanager/main.py:279:9 - Positional argument cannot follow keyword argument.

applications/datamanager/src/datamanager/clients.py

[error] 28-28: Ruff format failed. Command: 'ruff format'. Parse error in applications/datamanager/src/datamanager/clients.py:28:1 - Expected an indented block after class definition.

🔇 Additional comments (10)
libraries/python/src/internal/loss_functions.py (3)

9-11: Confirm change to default quantiles (from 0.25/0.5/0.75 to 0.1/0.5/0.9).

This is a behavioral change from the previous implementation (predictionengine) and will affect training dynamics for any caller not explicitly passing quantiles.

If this wasn’t intentional, revert to the prior defaults:

-    if quantiles is None:
-        quantiles = [0.1, 0.5, 0.9]
+    if quantiles is None:
+        quantiles = [0.25, 0.5, 0.75]

28-28: Confirm intentional change: returning the mean across quantiles (new) vs sum (previous).

The previous implementation summed per-quantile means; this one averages across quantiles. That changes loss scale by a factor of len(quantiles) and can impact training unless learning rates/expectations were adjusted.

If you want backward compatibility with the old behavior, apply:

-    return errors_total.div(len(quantiles))  # shape: (1,)
+    return errors_total  # shape: (1,)

20-26: Good: accumulation uses the returned Tensor from .add().

Tinygrad ops return new tensors; reassigning to errors_total is the correct pattern.

libraries/python/src/internal/lstm_network.py (3)

25-30: Constructor now uses explicit kwargs for LSTMCell — good change
This improves readability and guards against positional arg ordering changes.


76-78: Good guard against empty timestep outputs
Raising early prevents a later stack/indexing failure and keeps the error message clear.


32-36: Forward signature/state shape changed — call sites verified, no action required

Checked the repo: the LSTM.forward signature returns/accepts (hidden_state:list[Tensor], cell_state:list[Tensor]) and the only call sites are already using the tuple-of-two convention.

Files inspected:

  • libraries/python/src/internal/lstm_network.py — forward signature returns (hidden_state, cell_state) as lists.
  • libraries/python/src/internal/tft_model.py — uses: encoder_output, (h_n, c_n) = self.lstm_encoder.forward(...) and passes (h_n, c_n) into the decoder.
  • libraries/python/tests/test_lstm_network.py — tests call lstm.forward(...) and assert the returned state is a tuple of length 2.

No other usages of LSTM.forward or state= were found. Since callers and tests already match the new per-layer list shape, no changes are required.

applications/datamanager/src/datamanager/clients.py (1)

29-32: Good: fail fast on missing Alpaca credentials

Early validation with a clear ValueError is appropriate; this prevents confusing runtime failures.

applications/datamanager/src/datamanager/main.py (3)

59-63: Good: Alpaca client wired at startup with env-driven credentials

This aligns with fail-fast behavior given the constructor’s validation. Ensure deployment envs set both variables to avoid startup failure.


216-220: Sane default for fetch_date

Defaulting to “today” in America/New_York when fetch_date is None is sensible and keeps behavior explicit.


297-336: Endpoint looks good; status handling and logging are clear

The /calendar endpoint behavior is straightforward: 404 on empty, 200 with JSON on success, 500 on exception. Once the client’s calendar serialization is hardened (see clients.py), this route should be robust.

Comment on lines +27 to +28
class AlpacaClient:
class AlpacaClient:
def __init__(self, alpaca_api_key_id: str, alpaca_api_secret: str) -> None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix duplicated class declaration causing SyntaxError (CI is failing)

There are two consecutive class AlpacaClient: lines. Ruff/CI error confirms a parse error at Line 28. Remove the duplicate declaration.

Apply:

-class AlpacaClient:
 class AlpacaClient:
     def __init__(self, alpaca_api_key_id: str, alpaca_api_secret: str) -> None:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class AlpacaClient:
class AlpacaClient:
def __init__(self, alpaca_api_key_id: str, alpaca_api_secret: str) -> None:
class AlpacaClient:
def __init__(self, alpaca_api_key_id: str, alpaca_api_secret: str) -> None:
🧰 Tools
🪛 Ruff (0.12.2)

28-28: SyntaxError: Expected an indented block after class definition

🪛 GitHub Actions: Code quality check

[error] 28-28: Ruff format failed. Command: 'ruff format'. Parse error in applications/datamanager/src/datamanager/clients.py:28:1 - Expected an indented block after class definition.

🤖 Prompt for AI Agents
In applications/datamanager/src/datamanager/clients.py around lines 27 to 29,
there are two consecutive identical class declarations "class AlpacaClient:"
causing a SyntaxError; remove the duplicate declaration so only a single "class
AlpacaClient:" remains before the __init__ definition and ensure indentation of
the __init__ method follows that single class declaration.

Comment on lines 255 to 262
return create_cloud_event_success(
application_name="datamanager",
event_metadata=["equity", "bars", "created"],
data={
"date": request_summary_date,
"date": request_fetch_date,
"count": count,
},
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Avoid overriding CloudEvent’s timestamp: rename “date” in data payload

create_cloud_event_success already injects a "date" timestamp into the data payload. Providing "date" here will override it. Use a distinct key (e.g., "request_date").

-    return create_cloud_event_success(
-        application_name="datamanager",
-        event_metadata=["equity", "bars", "created"],
-        data={
-            "date": request_fetch_date,
-            "count": count,
-        },
-    )
+    return create_cloud_event_success(
+        application_name="datamanager",
+        event_metadata=["equity", "bars", "created"],
+        data={
+            "request_date": request_fetch_date,
+            "count": count,
+        },
+    )

Would you like me to sweep the codebase for other CloudEvent payloads that might be overriding this reserved key?


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== Searching for occurrences of 'create_cloud_event_success' ==="
rg -n --hidden --no-ignore -S "create_cloud_event_success" || true

echo
echo "=== Searching for definition 'def create_cloud_event_success' ==="
rg -n --hidden --no-ignore -S "def create_cloud_event_success" || true

echo
echo "=== Showing context (±8 lines) around matches ==="
rg -n -C8 --hidden --no-ignore -S "create_cloud_event_success" || true

# If files found, list the files and show occurrences of 'date' inside those files to check whether the helper inserts a 'date' key
files=$(rg -l --hidden --no-ignore -S "create_cloud_event_success" || true)
if [ -n "$files" ]; then
  echo
  echo "=== Inspecting 'date' mentions in files that reference create_cloud_event_success ==="
  for f in $files; do
    echo "---- $f ----"
    rg -n --hidden --no-ignore -S "date" "$f" || true
    echo
  done
else
  echo
  echo "No files reference 'create_cloud_event_success' (or rg didn't find matches)."
fi

Length of output: 14304


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== Showing libraries/python/src/internal/cloud_event.py ==="
nl -ba libraries/python/src/internal/cloud_event.py | sed -n '1,240p'

Length of output: 313


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== libraries/python/src/internal/cloud_event.py ==="
sed -n '1,240p' libraries/python/src/internal/cloud_event.py || true

Length of output: 1191


Avoid overriding CloudEvent's timestamp — rename payload "date" to "request_date"

create_cloud_event_success injects "date" into event.data and merges caller data after it ({"date": ..., **data}), so any "date" key you pass will override the generated timestamp. Rename the payload key to avoid accidental overrides.

Files to review:

  • libraries/python/src/internal/cloud_event.py — create_cloud_event_success sets "date" before merging caller data.
  • applications/datamanager/src/datamanager/main.py (lines ~255–262) — currently passes "date": request_fetch_date.
  • applications/positionmanager/src/positionmanager/main.py (lines ~245–252 and ~279–286) — passes "date" in event payloads.

Suggested change (datamanager):

-    return create_cloud_event_success(
-        application_name="datamanager",
-        event_metadata=["equity", "bars", "created"],
-        data={
-            "date": request_fetch_date,
-            "count": count,
-        },
-    )
+    return create_cloud_event_success(
+        application_name="datamanager",
+        event_metadata=["equity", "bars", "created"],
+        data={
+            "request_date": request_fetch_date,
+            "count": count,
+        },
+    )

Also review the positionmanager occurrences and decide whether those should intentionally override the CloudEvent timestamp or be renamed (e.g., event_date/request_date).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return create_cloud_event_success(
application_name="datamanager",
event_metadata=["equity", "bars", "created"],
data={
"date": request_summary_date,
"date": request_fetch_date,
"count": count,
},
)
return create_cloud_event_success(
application_name="datamanager",
event_metadata=["equity", "bars", "created"],
data={
"request_date": request_fetch_date,
"count": count,
},
)
🤖 Prompt for AI Agents
In applications/datamanager/src/datamanager/main.py around lines 255 to 262, the
payload is passing "date": request_fetch_date which will override the
CloudEvent's generated timestamp; rename the payload key to "request_date" (or
similar, e.g., event_date) and update the data dict to use that new key so
create_cloud_event_success can safely inject its own "date"; additionally scan
the two occurrences in applications/positionmanager/src/positionmanager/main.py
(around lines 245–252 and 279–286) and either rename those payload keys or
confirm they intentionally override the event timestamp, then update
callers/tests accordingly.

Comment thread libraries/python/src/internal/lstm_network.py
Comment on lines 80 to 83
if len(outputs) == 1:
output_tensor = outputs[0].unsqueeze(1)
output_tensor = outputs[0].unsqueeze(dim=1)
else:
output_tensor = Tensor.stack(outputs[0], *outputs[1:], dim=1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Prefer positional arg for unsqueeze to avoid API mismatch
tinygrad’s unsqueeze often mirrors PyTorch’s signature but keyword support isn’t guaranteed; using a positional arg is safer across versions.

-        if len(outputs) == 1:
-            output_tensor = outputs[0].unsqueeze(dim=1)
+        if len(outputs) == 1:
+            output_tensor = outputs[0].unsqueeze(1)

Run this to see if keyword-based unsqueeze is used elsewhere (and decide whether to standardize):


🏁 Script executed:

#!/bin/bash
rg -n -S "unsqueeze\\(" -A 0 -B 2

Length of output: 1054


Prefer positional arg for unsqueeze to avoid API mismatch

tinygrad may not accept the keyword form (dim=…); standardize to positional calls. My search found one keyword usage; other occurrences already use positional.

  • libraries/python/src/internal/lstm_network.py:81 — change outputs[0].unsqueeze(dim=1) to outputs[0].unsqueeze(1)
  • libraries/python/src/internal/tft_model.py:145 — static_context.unsqueeze(1) (already positional)
  • libraries/python/src/internal/tft_model.py:163 — static_context.unsqueeze(1) (already positional)

Suggested diff:

-        if len(outputs) == 1:
-            output_tensor = outputs[0].unsqueeze(dim=1)
+        if len(outputs) == 1:
+            output_tensor = outputs[0].unsqueeze(1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if len(outputs) == 1:
output_tensor = outputs[0].unsqueeze(1)
output_tensor = outputs[0].unsqueeze(dim=1)
else:
output_tensor = Tensor.stack(outputs[0], *outputs[1:], dim=1)
if len(outputs) == 1:
output_tensor = outputs[0].unsqueeze(1)
else:
output_tensor = Tensor.stack(outputs[0], *outputs[1:], dim=1)
🤖 Prompt for AI Agents
In libraries/python/src/internal/lstm_network.py around lines 80 to 83, the call
uses a keyword argument unsqueeze(dim=1) which may be incompatible with
tinygrad's API; change outputs[0].unsqueeze(dim=1) to the positional form
outputs[0].unsqueeze(1) so it matches other usages and avoids the API mismatch.

@forstmeier forstmeier force-pushed the 07-28-restructure_python_application_and_library_resources branch from cd2b10c to 17c9a34 Compare August 15, 2025 03:03
@forstmeier forstmeier force-pushed the 07-28-restructure_python_application_and_library_resources branch from 17c9a34 to fd0bad4 Compare August 15, 2025 13:36
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (2)
libraries/python/src/internal/lstm_network.py (1)

68-73: Fix dropout: currently mutates recurrent state and isn’t applied to inter-layer input

  • Dropout is applied to hidden_state (corrupts recurrent state for the next timestep) and is not used as the input to the next layer (layer_input remains pre-dropout). This breaks the intended “between-layers” dropout semantics.
  • Calling train() on a Tensor is not a valid tinygrad pattern; rely on global training mode if needed.

Apply dropout to layer_input and leave hidden/cell states untouched:

-                if self.dropout_rate > 0.0 and index < self.layer_count - 1:
-                    hidden_state[index].train()
-                    hidden_state[index] = hidden_state[index].dropout(self.dropout_rate)
-
-                layer_input = layer_hidden_state
+                layer_input = layer_hidden_state
+                if self.dropout_rate > 0.0 and index < self.layer_count - 1:
+                    # Apply dropout between layers, not on the recurrent state.
+                    layer_input = layer_input.dropout(self.dropout_rate)
infrastructure/environment_variables.py (1)

8-32: Rename infra secret: ALPACA_API_SECRET → ALPACA_API_SECRET_KEY

Infra currently provides ALPACA_API_SECRET but application code reads ALPACA_API_SECRET_KEY — this mismatch will cause runtime failures. Update infra or callers so names match.

Files to update:

  • infrastructure/environment_variables.py — change the secret name and the mapping.
  • applications/models/src/models/get_alpaca_equity_bars.py — reads os.getenv("ALPACA_API_SECRET_KEY")
  • applications/models/src/models/get_alpaca_calendar.py — reads os.getenv("ALPACA_API_SECRET_KEY")

Suggested minimal change (infra):

--- a/infrastructure/environment_variables.py
+++ b/infrastructure/environment_variables.py
@@
- alpaca_api_secret = configuration.require_secret("ALPACA_API_SECRET")
+ alpaca_api_secret = configuration.require_secret("ALPACA_API_SECRET_KEY")
@@
-            ("ALPACA_API_SECRET", alpaca_api_secret),
+            ("ALPACA_API_SECRET_KEY", alpaca_api_secret),

Also ensure any deployment/configuration (Pulumi config, secret manager entries) is updated to use ALPACA_API_SECRET_KEY.

♻️ Duplicate comments (17)
Dockerfile.tests (1)

1-1: Align base image Python version with requires-python across workspace (likely 3.12.10)
Multiple pyprojects previously declared requires-python = "==3.12.10". Using python:3.13 can cause uv resolution to fail under strict constraints. If the workspace still pins 3.12.10, switch the base image; if you upgraded the constraint to 3.13, ignore this.

Proposed change:

-FROM python:3.13
+FROM python:3.12.10

Optionally slim:

-FROM python:3.13
+FROM python:3.12.10-slim

Verify the constraints:

#!/bin/bash
set -euo pipefail
echo "requires-python across workspace:"
rg -n --type toml --glob '**/pyproject.toml' '^\s*requires-python\s*=.*' -n -C1
libraries/python/tests/test_loss_functions.py (1)

53-61: Seed the RNG for reproducible tests; add negative tests for shape/quantile-count mismatches

  • Reproducibility: As written, tests are nondeterministic across runs. Seed the generator once so failures are reproducible.
  • Negative tests: You removed shape-mismatch tests, and the implementation currently doesn’t validate count/shape mismatches. Reintroducing these tests will catch silent broadcasting or indexing errors and guard future refactors.

Seed suggestion (outside this hunk, near Line 7):

rng = Generator(PCG64(42))

Add these tests to the bottom of the file (pairs with adding validation in the implementation; see next comment):

def test_quantile_loss_quantile_count_mismatch_raises() -> None:
    predictions = Tensor([[[1.0, 1.2, 1.3]]])  # 3 quantiles in predictions
    targets = Tensor([[2.0]])
    quantiles = [0.25, 0.5]  # only 2 quantiles provided
    with pytest.raises(ValueError, match="Quantile count mismatch"):
        quantile_loss(predictions, targets, quantiles)


def test_quantile_loss_shape_mismatch_raises() -> None:
    predictions = Tensor([[[1.0, 1.2, 1.3]]])  # (1,1,3)
    targets = Tensor([[2.0], [3.0]])          # (2,1) mismatched batch
    quantiles = [0.25, 0.5, 0.75]
    with pytest.raises(ValueError, match="Shape mismatch"):
        quantile_loss(predictions, targets, quantiles)

If you want, I can open a follow-up PR or push a commit wiring these in alongside the implementation validation.

libraries/python/src/internal/lstm_network.py (3)

80-82: Use positional arg for unsqueeze to avoid tinygrad API mismatch

Prefer positional form; some tinygrad versions don’t accept dim= keyword here.

-            output_tensor = outputs[0].unsqueeze(dim=1)
+            output_tensor = outputs[0].unsqueeze(1)

13-16: Don’t hard-enforce layer_count >= 3 — allow >= 1 (optionally warn for < 3)

Enforcing a minimum of 3 is a breaking change and blocks common 1–2 layer LSTMs. Keep default=3 but allow shallower configs; optionally warn.

-        minimum_layer_count = 3
-        if layer_count < minimum_layer_count:
-            message = f"Layer count must be at least {minimum_layer_count}"
-            raise ValueError(message)
+        if layer_count < 1:
+            raise ValueError("layer_count must be at least 1")
+        if layer_count < 3:
+            import warnings
+            warnings.warn(
+                "Using fewer than 3 LSTM layers is supported but may reduce capacity; "
+                "defaults and some models expect 3.",
+                UserWarning,
+            )

If you intentionally require 3+, please confirm all call sites/tests never pass 1–2. To verify:

#!/bin/bash
set -euo pipefail
rg -nP '\bLSTM\s*\(' -C3
rg -nP 'layer_count\s*=\s*[12]\b' -n -C2

49-50: Validate external state length to match layer_count

Guard against mismatched state lists to avoid indexing errors and silent shape bugs.

         else:
             hidden_state, cell_state = state
+            expected_len = self.layer_count
+            if len(hidden_state) != expected_len or len(cell_state) != expected_len:
+                raise ValueError(
+                    f"State lengths must equal layer_count ({expected_len}); "
+                    f"got hidden={len(hidden_state)}, cell={len(cell_state)}"
+                )
applications/models/src/models/train_tft_model.py (4)

1-11: Fix import sorting to satisfy pipeline requirements.

The pipeline is failing due to Ruff I001 import ordering violation. Imports need to be grouped and sorted: standard library, third-party, then local imports.

Apply this diff to fix the import order:

-from datetime import datetime
-from zoneinfo import ZoneInfo
-
-import polars as pl
-from flytekit import task, workflow
-from internal.dataset import TemporalFusionTransformerDataset
-from internal.tft_model import Parameters, TemporalFusionTransformer
-from loguru import logger
-
-import wandb
+from datetime import datetime
+from zoneinfo import ZoneInfo
+
+import polars as pl
+from flytekit import task, workflow
+from loguru import logger
+import wandb
+
+from internal.dataset import TemporalFusionTransformerDataset
+from internal.tft_model import Parameters, TemporalFusionTransformer

20-27: Move wandb initialization inside train_model to avoid side effects.

Module-level wandb initialization creates side effects during import that can break Flyte task registration and distributed execution.

Apply this diff to remove module-level wandb initialization:

-if wandb.run is not None:
-    wandb.finish()  # close active run if it exists
-
-run = wandb.init(
-    project="Pocket Size Fund",
-    config=configuration,
-    name=f"tft-model-run-{datetime.now(tz=ZoneInfo('America/New_York')).strftime('%Y-%m-%d_%H-%M-%S')}",
-)
+# wandb run will be initialized within train_model to avoid import-time side effects

And update the train_model function to initialize wandb locally (see next comment).


84-99: Fix multiple issues in train_model function.

Several issues need to be addressed:

  1. Handle the return type from model.train() which returns {"losses": list[float]} according to the TFT model interface
  2. Move wandb initialization into this function to avoid global state conflicts
  3. Use total_seconds() for accurate timing calculation

Apply this diff:

     losses = model.train(
         inputs_list=batches,
         epoch_count=epoch_count,
         learning_rate=learning_rate,
     )

-    for loss in losses:
-        run.log({"loss": loss})
+    # Initialize wandb run within the task
+    if wandb.run is not None:
+        wandb.finish()
+    run = wandb.init(
+        project="Pocket Size Fund",
+        config=configuration,
+        name=f"tft-model-run-{datetime.now(tz=timezone).strftime('%Y-%m-%d_%H-%M-%S')}",
+    )
+
+    # Handle dict return type from model.train
+    loss_list = losses["losses"] if isinstance(losses, dict) else losses
+    for loss_value in loss_list:
+        run.log({"loss": loss_value})

     run.finish()

-    runtime_seconds = (datetime.now(tz=timezone) - start_time).seconds
+    runtime_seconds = int((datetime.now(tz=timezone) - start_time).total_seconds())

111-116: Fix validation batch construction to use correct input length.

The validation batches should use model.parameters.input_length or the model's input_length attribute, not model.batch_size (which doesn't exist in the TFT model interface).

Based on the TFT model interface from the relevant code snippets, apply this diff:

     batches = data.get_batches(
         data_type="validation",
         validation_split=validation_split,
-        input_length=model.input_length,
-        output_length=model.output_length,
+        input_length=model.parameters.input_length,
+        output_length=model.parameters.output_length,
     )
libraries/python/src/internal/mhsa_network.py (2)

13-21: Guard against invalid heads_count/embedding_size and bound dropout_rate

Avoid ZeroDivisionError when heads_count == 0 and validate dropout_rate range. Also prefer casting dropout_rate to float once.

     ) -> None:
-        if embedding_size % heads_count != 0:
+        if heads_count <= 0:
+            raise ValueError("heads_count must be a positive integer")
+        if embedding_size <= 0:
+            raise ValueError("embedding_size must be a positive integer")
+        if embedding_size % heads_count != 0:
             message = "Embedding dimension must be divisible by heads count"
             raise ValueError(message)
 
         self.heads_count = heads_count
         self.embedding_size = embedding_size
         self.heads_dimension = embedding_size // heads_count
-        self.dropout_rate = dropout_rate
+        self.dropout_rate = float(dropout_rate)
+        if not (0.0 <= self.dropout_rate < 1.0):
+            raise ValueError("dropout_rate must be in the range [0.0, 1.0).")

30-33: Fast-fail on input embedding mismatch

Raise a clear error if the last dim of inputs doesn’t match embedding_size.

     def forward(self, inputs: Tensor) -> tuple[Tensor, Tensor]:
-        batch_size, sequence_length, _ = inputs.shape
+        if inputs.shape[-1] != self.embedding_size:
+            raise ValueError(
+                f"Last dimension of inputs must equal embedding_size "
+                f"({inputs.shape[-1]} != {self.embedding_size})"
+            )
+        batch_size, sequence_length, _ = inputs.shape
libraries/python/tests/test_dataset.py (1)

81-134: Test coverage is incomplete for batch generation.

The current test only covers data_type="predict". The get_batches method likely supports "train" and "validate" modes as well, which should be tested to ensure complete coverage.

Would you like me to generate additional test cases for the "train" and "validate" data types to ensure comprehensive coverage of the batch generation functionality?

infrastructure/environment_variables.py (2)

22-24: Export APCA_ vars too to align with Alpaca’s standard and mixed callers*

There’s mixed usage of ALPACA_* and APCA_* names across the codebase. Exporting APCA_* here avoids breaking services while you migrate callers.

Apply this diff:

@@
-            ("ALPACA_API_KEY_ID", alpaca_api_key_id),
-            ("ALPACA_API_SECRET", alpaca_api_secret),
+            ("ALPACA_API_KEY_ID", alpaca_api_key_id),
+            ("ALPACA_API_SECRET", alpaca_api_secret),
+            ("ALPACA_API_SECRET_KEY", alpaca_api_secret),
+            ("APCA_API_KEY_ID", alpaca_api_key_id),
+            ("APCA_API_SECRET_KEY", alpaca_api_secret),

Optionally, plan a follow-up PR to unify all consumers on APCA_* and then remove ALPACA_*.


8-9: Mismatch with consumers: ALPACA_API_SECRET vs ALPACA_API_SECRET_KEY

Downstream code (e.g., models scripts) reads ALPACA_API_SECRET_KEY, but this file only provisions ALPACA_API_SECRET. This will yield missing credentials at runtime.

Apply this safe, backwards-compatible change to export both variants:

@@
-            ("ALPACA_API_KEY_ID", alpaca_api_key_id),
-            ("ALPACA_API_SECRET", alpaca_api_secret),
+            ("ALPACA_API_KEY_ID", alpaca_api_key_id),
+            ("ALPACA_API_SECRET", alpaca_api_secret),
+            ("ALPACA_API_SECRET_KEY", alpaca_api_secret),
applications/models/src/models/get_alpaca_equity_bars.py (2)

22-32: Credential env-var mismatch and lack of validation; add fallbacks and fail fast

This script reads ALPACA_API_SECRET_KEY, but infra exports ALPACA_API_SECRET. Also, if env vars are unset, clients are created with None. Validate and support both names.

Apply this diff:

@@
-    alpaca_trading_client = TradingClient(
-        api_key=os.getenv("ALPACA_API_KEY_ID"),
-        secret_key=os.getenv("ALPACA_API_SECRET_KEY"),
-        paper=os.getenv("ALPACA_PAPER", "true").lower() == "true",
-    )
+    api_key_id = os.getenv("ALPACA_API_KEY_ID") or os.getenv("APCA_API_KEY_ID")
+    api_secret = (
+        os.getenv("ALPACA_API_SECRET_KEY")
+        or os.getenv("ALPACA_API_SECRET")
+        or os.getenv("APCA_API_SECRET_KEY")
+    )
+    if not api_key_id or not api_secret:
+        raise RuntimeError(
+            "Missing Alpaca API credentials. Set ALPACA_API_KEY_ID and "
+            "ALPACA_API_SECRET (or *_SECRET_KEY/APCA_* equivalents)."
+        )
+
+    alpaca_trading_client = TradingClient(
+        api_key=api_key_id,
+        secret_key=api_secret,
+        paper=os.getenv("ALPACA_PAPER", "true").lower() == "true",
+    )
@@
-    alpaca_data_client = StockHistoricalDataClient(
-        api_key=os.getenv("ALPACA_API_KEY_ID"),
-        secret_key=os.getenv("ALPACA_API_SECRET_KEY"),
-        sandbox=os.getenv("ALPACA_PAPER", "true").lower() == "true",
-    )
+    alpaca_data_client = StockHistoricalDataClient(
+        api_key=api_key_id,
+        secret_key=api_secret,
+    )

Note: See next comment about verifying whether StockHistoricalDataClient supports a sandbox parameter.


93-100: Fix timestamp parsing: datetime.fromisoformat() can’t parse 'Z'

Alpaca timestamps commonly end with 'Z'. datetime.fromisoformat('...Z') raises ValueError. Normalize 'Z' to '+00:00' before parsing.

Apply this diff:

-        equity_bars_data = equity_bars_data.with_columns(
-            (
-                pl.col("timestamp").map_elements(
-                    lambda x: int(datetime.fromisoformat(x).timestamp() * 1000)
-                )
-            ).alias("timestamp")
-        )
+        equity_bars_data = equity_bars_data.with_columns(
+            (
+                pl.col("timestamp").map_elements(
+                    lambda x: int(datetime.fromisoformat(x.replace("Z", "+00:00")).timestamp() * 1000)
+                )
+            ).alias("timestamp")
+        )
applications/models/src/models/get_alpaca_calendar.py (1)

13-17: Credential env-var mismatch and missing validation

This script reads ALPACA_API_SECRET_KEY but infra exports ALPACA_API_SECRET. Add fallbacks and fail fast when credentials are missing to avoid confusing 401s.

Apply this diff:

@@
-    alpaca_client = TradingClient(
-        api_key=os.getenv("ALPACA_API_KEY_ID"),
-        secret_key=os.getenv("ALPACA_API_SECRET_KEY"),
-        paper=os.getenv("ALPACA_PAPER", "true").lower() == "true",
-    )
+    api_key_id = os.getenv("ALPACA_API_KEY_ID") or os.getenv("APCA_API_KEY_ID")
+    api_secret = (
+        os.getenv("ALPACA_API_SECRET_KEY")
+        or os.getenv("ALPACA_API_SECRET")
+        or os.getenv("APCA_API_SECRET_KEY")
+    )
+    if not api_key_id or not api_secret:
+        raise RuntimeError(
+            "Missing Alpaca API credentials. Set ALPACA_API_KEY_ID and "
+            "ALPACA_API_SECRET (or *_SECRET_KEY/APCA_* equivalents)."
+        )
+    alpaca_client = TradingClient(
+        api_key=api_key_id,
+        secret_key=api_secret,
+        paper=os.getenv("ALPACA_PAPER", "true").lower() == "true",
+    )
🧹 Nitpick comments (14)
Dockerfile.tests (1)

30-30: Pin to lockfile during CI installs

To ensure reproducible installs in CI, consider enforcing the lockfile.

Apply this diff:

-RUN uv sync --all-packages --dev
+RUN uv sync --all-packages --dev --frozen
libraries/python/tests/test_loss_functions.py (4)

11-16: Strengthen the basic test by asserting non-negativity (pinball loss property)

Quantile loss should be non-negative when quantiles ∈ [0,1]. Add an assertion to make this explicit.

     loss = quantile_loss(predictions, targets, quantiles)
 
-    assert isinstance(loss, Tensor)
+    assert isinstance(loss, Tensor)
+    assert loss.numpy() >= 0.0  # pinball loss is non-negative
     assert len(loss.shape) == 0 or loss.shape in [(), (0,), (1,)]

33-40: Assert perfect prediction is ~0 (use tolerance) instead of only non-negative

This is a good place to be strict: perfect prediction should yield zero loss. Using a tolerance keeps the test robust to tinygrad/numpy scalar vs array nuances.

     loss = quantile_loss(predictions, targets, quantiles)
 
-    assert loss.numpy() >= 0.0
+    # Use tolerance to avoid flakiness across scalar/array representations
+    assert np.allclose(loss.numpy(), 0.0, atol=1e-7)

64-69: Broaden invalid-quantiles coverage with parametrize

Also test the <0 boundary to complement the >1 case and reduce duplication.

-def test_quantile_loss_invalid_quantiles() -> None:
-    predictions = Tensor([[[1.0, 1.2, 1.3]]])
-    targets = Tensor([[2.0]])
-    quantiles = [0.25, 1.5, 0.75]  # Invalid quantile > 1
-    with pytest.raises(ValueError, match="All quantiles must be between 0 and 1"):
-        quantile_loss(predictions, targets, quantiles)
+@pytest.mark.parametrize("bad_q", [-0.1, 1.5])
+def test_quantile_loss_invalid_quantiles(bad_q: float) -> None:
+    predictions = Tensor([[[1.0, 1.2, 1.3]]])
+    targets = Tensor([[2.0]])
+    quantiles = [0.25, bad_q, 0.75]
+    with pytest.raises(ValueError, match="All quantiles must be between 0 and 1"):
+        quantile_loss(predictions, targets, quantiles)

3-3: Pair tests with implementation validation: enforce quantile count and shape constraints

The current implementation in libraries/python/src/internal/loss_functions.py (per the snippet) lacks:

  • A check that predictions.shape[-1] equals len(quantiles)
  • A check that predictions.shape[:2] matches targets.shape

Without these, the new tests for mismatches would fail to catch silent broadcasting/indexing bugs.

Proposed update to libraries/python/src/internal/loss_functions.py (outside this file):

def quantile_loss(
    predictions: Tensor,  # shape: (batch_size, output_size, len(quantiles))
    targets: Tensor,      # shape: (batch_size, output_size)
    quantiles: list[float] | None = None,
) -> Tensor:
    if quantiles is None:
        quantiles = [0.1, 0.5, 0.9]

    if not all(0 <= q <= 1 for q in quantiles):
        raise ValueError("All quantiles must be between 0 and 1")

    # NEW: shape validations
    if predictions.shape[-1] != len(quantiles):
        raise ValueError(
            f"Quantile count mismatch: predictions last dimension ({predictions.shape[-1]}) "
            f"vs number of quantiles ({len(quantiles)})"
        )
    if predictions.shape[:2] != targets.shape:
        raise ValueError(
            f"Shape mismatch: predictions batch/output {predictions.shape[:2]} "
            f"vs targets {targets.shape}"
        )

    errors_total = Tensor.zeros((1,))
    for index, quantile in enumerate(quantiles):
        error = targets.sub(predictions[:, :, index])
        quantile_tensor = Tensor([quantile])
        errors_total = errors_total.add(
            Tensor.where(
                error > 0,
                quantile_tensor.mul(error),
                (quantile_tensor.sub(1)).mul(error),
            ).mean()
        )

    return errors_total.div(len(quantiles))  # shape: (1,)

Would you like me to push commits adding the validations and the two negative tests?

libraries/python/src/internal/lstm_network.py (3)

23-30: Avoid shadowing the input_size parameter for readability

Use a local variable for per-layer input size.

-        for index in range(layer_count):
-            input_size = input_size if index == 0 else self.hidden_size
-            self.layers.append(
-                LSTMCell(
-                    input_size=input_size,
-                    hidden_size=self.hidden_size,
-                )
-            )
+        for index in range(layer_count):
+            layer_input_size = input_size if index == 0 else self.hidden_size
+            self.layers.append(
+                LSTMCell(
+                    input_size=layer_input_size,
+                    hidden_size=self.hidden_size,
+                )
+            )

18-20: Validate dropout_rate range

Ensure the configured dropout probability is valid.

         self.hidden_size = hidden_size
         self.layer_count = layer_count
         self.dropout_rate = dropout_rate
+        if not (0.0 <= self.dropout_rate < 1.0):
+            raise ValueError("dropout_rate must be in [0.0, 1.0).")

82-83: Minor: simplify stacking call

Cleaner and avoids special-casing the first element.

-            output_tensor = Tensor.stack(outputs[0], *outputs[1:], dim=1)
+            output_tensor = Tensor.stack(*outputs, dim=1)
applications/models/src/models/train_tft_model.py (1)

38-38: Use total_seconds() for accurate timing calculations.

Using .seconds only returns the seconds component and ignores sub-second precision and minutes/hours. Use .total_seconds() for accurate timing.

Apply this diff to fix timing calculations:

-    runtime_seconds = (datetime.now(tz=timezone) - start_time).seconds
+    runtime_seconds = int((datetime.now(tz=timezone) - start_time).total_seconds())

This applies to lines 38, 95, 122, and 136.

Also applies to: 95-95, 122-122, 136-136

libraries/python/src/internal/mhsa_network.py (2)

28-29: Use a Python float for scale to avoid dtype/device friction

Scale is a constant; storing it as a Tensor is unnecessary and can cause minor dtype/device mismatches. A plain float works with broadcasting.

-        self.scale = Tensor(self.heads_dimension**0.5, dtype=dtypes.float32)
+        self.scale = float(self.heads_dimension ** 0.5)

No other code changes needed; division by a Python float will broadcast as expected.

Also applies to: 44-46


6-12: Add a concise class docstring (shapes, returns, dropout semantics)

Helps downstream users and aligns with internal library standards.

-class MultiHeadSelfAttentionNetwork:
+class MultiHeadSelfAttentionNetwork:
+    """
+    Multi-head self-attention block.
+
+    Args:
+        heads_count: Number of attention heads.
+        embedding_size: Input/output embedding dimension per token.
+        dropout_rate: Dropout probability applied to attention weights and final projection.
+
+    Forward:
+        inputs: Tensor of shape (batch, seq_len, embedding_size)
+        Returns: (output, attention_weights)
+            - output shape: (batch, seq_len, embedding_size)
+            - attention_weights shape: (batch, heads_count, seq_len, seq_len)
+    """
infrastructure/environment_variables.py (1)

20-32: Preserve secrecy of the combined env dict explicitly

Even though secrets generally propagate through Pulumi Outputs, explicitly marking the final dict as secret avoids accidental exposure.

Apply this diff:

-    return pulumi.Output.all(
+    return pulumi.Output.secret(pulumi.Output.all(
         [
             ("ALPACA_API_KEY_ID", alpaca_api_key_id),
             ("ALPACA_API_SECRET", alpaca_api_secret),
             ("DATA_BUCKET_NAME", data_bucket_name),
             ("POLYGON_API_KEY", polygon_api_key),
             ("DUCKDB_ACCESS_KEY", duckdb_access_key),
             ("DUCKDB_SECRET", duckdb_secret),
             ("AWS_REGION", aws_region),
             ("DUCKDB_USER_ACCESS_KEY_ID", duckdb_user_access_key.id),
             ("DUCKDB_USER_ACCESS_KEY_SECRET", duckdb_user_access_key.secret),
         ]
-    ).apply(lambda secrets: dict(secrets))
+    ).apply(lambda secrets: dict(secrets)))
applications/models/src/models/get_alpaca_equity_bars.py (1)

56-71: Prefer enum constants over constructing from strings

TimeFrameUnit("Day") and Adjustment("All") rely on string coercion. Use constants to avoid subtle breakage across versions.

Apply this diff:

-                    timeframe=TimeFrame(
-                        amount=1,
-                        unit=TimeFrameUnit("Day"),
-                    ),
-                    adjustment=Adjustment("All"),
+                    timeframe=TimeFrame(amount=1, unit=TimeFrameUnit.Day),
+                    adjustment=Adjustment.ALL,
applications/models/src/models/get_alpaca_calendar.py (1)

22-30: Wrap network call with error handling to surface actionable failures

Wrap get_calendar in try/except to log and preserve stack traces. Keeps success path unchanged.

Apply this diff:

-    calendars: list[Calendar] = cast(
-        "list[Calendar]",
-        alpaca_client.get_calendar(
-            GetCalendarRequest(
-                start=start,
-                end=end,
-            )
-        ),
-    )
+    try:
+        calendars: list[Calendar] = cast(
+            "list[Calendar]",
+            alpaca_client.get_calendar(
+                GetCalendarRequest(
+                    start=start,
+                    end=end,
+                )
+            ),
+        )
+    except Exception as exc:
+        logger.exception(f"Failed to fetch Alpaca calendar: {exc}")
+        raise
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cd2b10c and fd0bad4.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (78)
  • .claude/settings.local.json (1 hunks)
  • .github/workflows/launch_infrastructure.yaml (1 hunks)
  • .github/workflows/teardown_infrastructure.yaml (1 hunks)
  • .gitignore (2 hunks)
  • .mise.toml (2 hunks)
  • Dockerfile.tests (1 hunks)
  • README.md (0 hunks)
  • application/datamanager/.dockerignore (0 hunks)
  • application/datamanager/Dockerfile (0 hunks)
  • application/datamanager/Dockerfile.test (0 hunks)
  • application/datamanager/compose.yaml (0 hunks)
  • application/datamanager/features/environment.py (0 hunks)
  • application/datamanager/features/equity_bars.feature (0 hunks)
  • application/datamanager/features/health.feature (0 hunks)
  • application/datamanager/features/steps/equity_bars_steps.py (0 hunks)
  • application/datamanager/features/steps/health_steps.py (0 hunks)
  • application/datamanager/mise.toml (0 hunks)
  • application/datamanager/pyproject.toml (0 hunks)
  • application/datamanager/src/datamanager/clients.py (0 hunks)
  • application/datamanager/src/datamanager/main.py (0 hunks)
  • application/datamanager/tests/test_datamanager_main.py (0 hunks)
  • application/datamanager/tests/test_datamanager_models.py (0 hunks)
  • application/positionmanager/Dockerfile (0 hunks)
  • application/positionmanager/pyproject.toml (0 hunks)
  • application/positionmanager/src/positionmanager/__init__.py (0 hunks)
  • application/positionmanager/src/positionmanager/clients.py (0 hunks)
  • application/positionmanager/src/positionmanager/main.py (0 hunks)
  • application/positionmanager/src/positionmanager/portfolio.py (0 hunks)
  • application/positionmanager/tests/test_positionmanager_main.py (0 hunks)
  • application/predictionengine/Dockerfile (0 hunks)
  • application/predictionengine/compose.yaml (0 hunks)
  • application/predictionengine/pyproject.toml (0 hunks)
  • application/predictionengine/src/predictionengine/dataset.py (0 hunks)
  • application/predictionengine/src/predictionengine/gated_residual_network.py (0 hunks)
  • application/predictionengine/src/predictionengine/loss_function.py (0 hunks)
  • application/predictionengine/src/predictionengine/main.py (0 hunks)
  • application/predictionengine/src/predictionengine/miniature_temporal_fusion_transformer.py (0 hunks)
  • application/predictionengine/src/predictionengine/multi_head_self_attention.py (0 hunks)
  • application/predictionengine/src/predictionengine/post_processor.py (0 hunks)
  • application/predictionengine/src/predictionengine/ticker_embedding.py (0 hunks)
  • application/predictionengine/tests/test_dataset.py (0 hunks)
  • application/predictionengine/tests/test_gated_residual_network.py (0 hunks)
  • application/predictionengine/tests/test_post_processor.py (0 hunks)
  • application/predictionengine/tests/test_predictionengine_main.py (0 hunks)
  • application/predictionengine/tests/test_ticker_embedding.py (0 hunks)
  • applications/datamanager/pyproject.toml (1 hunks)
  • applications/models/pyproject.toml (1 hunks)
  • applications/models/src/models/get_alpaca_calendar.py (1 hunks)
  • applications/models/src/models/get_alpaca_equity_bars.py (1 hunks)
  • applications/models/src/models/train_tft_model.py (1 hunks)
  • applications/portfoliomanager/pyproject.toml (1 hunks)
  • cli/datamanager.py (0 hunks)
  • cli/pyproject.toml (0 hunks)
  • infrastructure/environment_variables.py (2 hunks)
  • infrastructure/images.py (1 hunks)
  • libraries/python/pyproject.toml (1 hunks)
  • libraries/python/src/internal/cloud_event.py (1 hunks)
  • libraries/python/src/internal/dataset.py (1 hunks)
  • libraries/python/src/internal/dates.py (2 hunks)
  • libraries/python/src/internal/equity_bar.py (1 hunks)
  • libraries/python/src/internal/loss_functions.py (1 hunks)
  • libraries/python/src/internal/lstm_network.py (2 hunks)
  • libraries/python/src/internal/mhsa_network.py (1 hunks)
  • libraries/python/src/internal/money.py (0 hunks)
  • libraries/python/src/internal/summaries.py (1 hunks)
  • libraries/python/src/internal/tft_model.py (1 hunks)
  • libraries/python/src/internal/variable_selection_network.py (1 hunks)
  • libraries/python/tests/test_dataset.py (1 hunks)
  • libraries/python/tests/test_dates.py (1 hunks)
  • libraries/python/tests/test_equity_bar.py (1 hunks)
  • libraries/python/tests/test_loss_functions.py (4 hunks)
  • libraries/python/tests/test_lstm_network.py (5 hunks)
  • libraries/python/tests/test_mhsa_network.py (5 hunks)
  • libraries/python/tests/test_variable_selection_network.py (1 hunks)
  • pyproject.toml (1 hunks)
  • workflows/fetch_data.py (0 hunks)
  • workflows/pyproject.toml (0 hunks)
  • workflows/train_predictionengine.py (0 hunks)
💤 Files with no reviewable changes (45)
  • cli/pyproject.toml
  • application/datamanager/.dockerignore
  • application/predictionengine/tests/test_post_processor.py
  • application/predictionengine/src/predictionengine/miniature_temporal_fusion_transformer.py
  • application/datamanager/features/environment.py
  • application/predictionengine/pyproject.toml
  • application/datamanager/compose.yaml
  • application/positionmanager/pyproject.toml
  • application/datamanager/features/steps/health_steps.py
  • application/positionmanager/src/positionmanager/init.py
  • application/datamanager/Dockerfile.test
  • application/predictionengine/tests/test_predictionengine_main.py
  • application/predictionengine/src/predictionengine/ticker_embedding.py
  • application/datamanager/Dockerfile
  • README.md
  • application/datamanager/features/health.feature
  • application/positionmanager/Dockerfile
  • application/datamanager/tests/test_datamanager_models.py
  • workflows/pyproject.toml
  • application/predictionengine/compose.yaml
  • application/datamanager/src/datamanager/clients.py
  • application/predictionengine/src/predictionengine/gated_residual_network.py
  • application/datamanager/mise.toml
  • application/datamanager/features/equity_bars.feature
  • application/positionmanager/tests/test_positionmanager_main.py
  • application/positionmanager/src/positionmanager/portfolio.py
  • application/datamanager/pyproject.toml
  • workflows/train_predictionengine.py
  • application/predictionengine/src/predictionengine/loss_function.py
  • application/predictionengine/Dockerfile
  • application/predictionengine/src/predictionengine/dataset.py
  • application/datamanager/src/datamanager/main.py
  • application/datamanager/tests/test_datamanager_main.py
  • application/positionmanager/src/positionmanager/clients.py
  • libraries/python/src/internal/money.py
  • application/predictionengine/src/predictionengine/post_processor.py
  • application/predictionengine/tests/test_gated_residual_network.py
  • application/positionmanager/src/positionmanager/main.py
  • application/predictionengine/src/predictionengine/main.py
  • cli/datamanager.py
  • application/predictionengine/tests/test_ticker_embedding.py
  • application/datamanager/features/steps/equity_bars_steps.py
  • workflows/fetch_data.py
  • application/predictionengine/tests/test_dataset.py
  • application/predictionengine/src/predictionengine/multi_head_self_attention.py
✅ Files skipped from review due to trivial changes (2)
  • applications/portfoliomanager/pyproject.toml
  • applications/datamanager/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (22)
  • libraries/python/src/internal/summaries.py
  • libraries/python/pyproject.toml
  • libraries/python/src/internal/cloud_event.py
  • infrastructure/images.py
  • .github/workflows/launch_infrastructure.yaml
  • libraries/python/src/internal/tft_model.py
  • pyproject.toml
  • .mise.toml
  • libraries/python/src/internal/variable_selection_network.py
  • .claude/settings.local.json
  • libraries/python/tests/test_dates.py
  • libraries/python/tests/test_mhsa_network.py
  • libraries/python/src/internal/dataset.py
  • .gitignore
  • libraries/python/src/internal/dates.py
  • libraries/python/tests/test_lstm_network.py
  • libraries/python/tests/test_variable_selection_network.py
  • .github/workflows/teardown_infrastructure.yaml
  • applications/models/pyproject.toml
  • libraries/python/src/internal/equity_bar.py
  • libraries/python/tests/test_equity_bar.py
  • libraries/python/src/internal/loss_functions.py
🧰 Additional context used
🧬 Code Graph Analysis (6)
libraries/python/tests/test_dataset.py (1)
libraries/python/src/internal/dataset.py (2)
  • TemporalFusionTransformerDataset (28-326)
  • get_dimensions (240-248)
libraries/python/tests/test_loss_functions.py (2)
libraries/python/src/internal/loss_functions.py (1)
  • quantile_loss (4-28)
application/predictionengine/src/predictionengine/loss_function.py (1)
  • quantile_loss (8-29)
applications/models/src/models/get_alpaca_equity_bars.py (4)
application/datamanager/src/datamanager/main.py (2)
  • get_equity_bars (149-202)
  • fetch_equity_bars (206-269)
workflows/train_predictionengine.py (1)
  • fetch_data (22-67)
application/datamanager/tests/test_datamanager_main.py (1)
  • test_fetch_equity_bars_success (199-226)
cli/datamanager.py (1)
  • get_equity_bars (54-59)
applications/models/src/models/train_tft_model.py (2)
libraries/python/src/internal/dataset.py (3)
  • TemporalFusionTransformerDataset (28-326)
  • get_dimensions (240-248)
  • get_batches (250-326)
libraries/python/src/internal/tft_model.py (5)
  • Parameters (19-33)
  • TemporalFusionTransformer (37-271)
  • train (192-225)
  • validate (227-246)
  • save (248-253)
libraries/python/src/internal/lstm_network.py (2)
application/predictionengine/src/predictionengine/long_short_term_memory.py (3)
  • LongShortTermMemory (5-69)
  • forward (22-69)
  • __init__ (6-20)
application/predictionengine/tests/test_long_short_term_memory.py (4)
  • test_lstm_forward (31-43)
  • test_lstm_consistency (81-92)
  • test_lstm_multiple_layers (58-67)
  • test_lstm_single_timestep (70-78)
libraries/python/src/internal/mhsa_network.py (2)
application/predictionengine/src/predictionengine/multi_head_self_attention.py (3)
  • MultiHeadSelfAttention (8-66)
  • __init__ (9-30)
  • forward (32-66)
application/predictionengine/tests/test_multi_head_self_attention.py (6)
  • test_multi_head_attention_different_heads (34-45)
  • test_multi_head_attention_forward (20-31)
  • test_multi_head_attention_batch_processing (67-75)
  • test_multi_head_attention_single_sequence (48-54)
  • test_multi_head_attention_initialization (11-17)
  • test_multi_head_attention_longer_sequences (57-64)
🪛 GitHub Check: Run Python quality checks
applications/models/src/models/train_tft_model.py

[failure] 1-10: Ruff (I001)
applications/models/src/models/train_tft_model.py:1:1: I001 Import block is un-sorted or un-formatted

🪛 GitHub Actions: Code quality check
applications/models/src/models/train_tft_model.py

[error] 1-1: Command 'ruff check --output-format=github .' failed. Ruff I001: Import block is un-sorted or un-formatted.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Python tests
🔇 Additional comments (17)
Dockerfile.tests (3)

20-28: Good caching-friendly COPY of workspace pyproject.toml files

Copying only the pyproject.toml files for each workspace member before uv sync is the right move to preserve Docker layer caching. Paths also reflect the new applications/* and libraries/python layout.


32-34: Nice: post-sync copy of full sources and inclusion of libraries tests

Copying full sources after uv sync keeps the dependency layer cacheable, and adding COPY libraries/ ensures internal library tests are available in the image.


20-24: All referenced pyproject.toml files are present — no action needed

Verified with the provided script output; all three files exist:

  • applications/datamanager/pyproject.toml — OK
  • applications/portfoliomanager/pyproject.toml — OK
  • applications/models/pyproject.toml — OK
libraries/python/tests/test_loss_functions.py (3)

3-3: Import path migration looks correct; ensure test environment resolves internal module

The import reflects the new internal library layout. Confirm that the test runner installs/paths include libraries/python/src so internal.loss_functions resolves under uv/pyproject configuration.


22-27: Multi-sample, multi-quantile case reads well

Covers the batched path with Q=3 and matches the new interface. No issues spotted.


43-49: Broader quantile coverage LGTM

Nice coverage for Q=5; this exercises the loop over quantiles and keeps the non-negativity check.

libraries/python/src/internal/mhsa_network.py (3)

37-43: LGTM on head reshaping and transposition

The view/transpose pipeline correctly produces (batch, heads, seq_len, head_dim) for Q/K/V; aligns with expected attention shapes.


44-49: Scaled dot-product attention is correct

Dividing by sqrt(d_k) and softmax along the last axis matches spec.


64-64: Overall: solid, readable implementation

Clear structure, minimal allocations, and returns both output and attention weights. With the input/param validations added, this should be robust.

libraries/python/tests/test_dataset.py (5)

1-2: LGTM!

Clean imports and appropriate testing module structure.


5-47: LGTM!

The test validates the basic instantiation and attribute presence after data loading. The test data includes all required columns with realistic financial data across multiple tickers and time periods.


49-79: LGTM!

Comprehensive validation of the dimensions dictionary structure, ensuring all required feature group keys are present in the returned dimensions.


111-115: Signature verified — get_batches accepts data_type, input_length, and output_length

The test call matches the implementation.

  • Location: libraries/python/src/internal/dataset.py — def get_batches(self, data_type: str = "train", validation_split: float = 0.8, input_length: int = 35, output_length: int = 7) -> list[dict[str, Tensor]] (defined at/around line 250).

126-134: Validate .shape assertions — OK (no change required)

get_batches constructs tinygrad.Tensor instances from numpy arrays, and tinygrad.Tensor (v0.10.3) exposes a .shape property — the assertions in test_dataset.py are therefore meaningful.

  • libraries/python/src/internal/dataset.py — get_batches: creates Tensor(...) for encoder_categorical_features, encoder_continuous_features, decoder_categorical_features, static_categorical_features, and targets.
  • libraries/python/tests/test_dataset.py — test_dataset_batches: asserts .shape[0] on those tensors.
applications/models/src/models/get_alpaca_equity_bars.py (3)

34-43: Check GetAssetsRequest.attributes type; prefer enums/list over string

Passing attributes="has_options" may be brittle if the SDK expects a list or enum values (e.g., AssetAttributes.HAS_OPTIONS).

Please confirm the expected type in the installed alpaca-py version. If it expects enums, adjust to:

from alpaca.trading.requests import GetAssetsRequest
from alpaca.trading.enums import AssetAttributes

GetAssetsRequest(
    status=AssetStatus.ACTIVE,
    asset_class=AssetClass.US_EQUITY,
    attributes=[AssetAttributes.HAS_OPTIONS],
)

112-118: LGTM: robust aggregation with saved-files guard

Good job tracking written files and guarding against the empty case before concatenation. Prevents file-not-found errors and clarifies logs.


28-32: No change required — StockHistoricalDataClient supports the sandbox parameter

Verified: alpaca-py’s StockHistoricalDataClient constructor includes a sandbox: bool parameter; the instantiation in applications/models/src/models/get_alpaca_equity_bars.py (the existing sandbox=os.getenv("ALPACA_PAPER", "true").lower() == "true") is valid.

Comment on lines +19 to +21
end = datetime.now(tz=ZoneInfo("America/New_York"))
start = end - timedelta(days=365 * 6)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm GetCalendarRequest expects date vs datetime

The Trading calendar API typically works with date-only parameters. Passing timezone-aware datetimes should work via SDK coercion, but confirm your alpaca-py version’s types to avoid subtle TZ issues.

Run:

If it expects date, consider start.date() / end.date().


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find alpaca-py in pyproject to infer version:"
rg -n -C2 -S 'alpaca-py' --type toml

echo
echo "Search for Calendar/GetCalendarRequest usage hints:"
rg -n -C2 -S 'GetCalendarRequest\('

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---- Show target file ----"
sed -n '1,240p' applications/models/src/models/get_alpaca_calendar.py || true

echo
echo "---- Search for GetCalendarRequest / GetCalendar usage ----"
rg -n --hidden -S 'GetCalendarRequest|GetCalendar' || true

echo
echo "---- Search for alpaca package usages/imports (python files, pyproject, requirements, lockfiles) ----"
rg -n --hidden -S "from .*alpaca|import .*alpaca|alpaca-py|alpaca_trade_api|alpaca" --glob '!**/node_modules/**' --glob '!**/.git/**' || true

echo
echo "---- Show pyproject.toml and poetry.lock if present ----"
for f in pyproject.toml poetry.lock Pipfile requirements.txt requirements-dev.txt; do
  if [ -f "$f" ]; then
    echo "---- $f ----"
    sed -n '1,240p' "$f"
  fi
done

echo
echo "---- Search installed package typing hints / stubs in repo (patterns like GetCalendarRequest) ----"
rg -n --hidden -S 'GetCalendarRequest' || true

Length of output: 9356


🌐 Web query:

alpaca-py "GetCalendarRequest" start end date datetime "alpaca.trading.requests"

💡 Result:

Short answer — use alpaca.trading.requests.GetCalendarRequest with Python date objects (datetime.date). You can pass datetime.datetime.date() if you have datetimes. Example usage:

Notes from the docs: GetCalendarRequest signature is
class alpaca.trading.requests.GetCalendarRequest(*, start: Optional[date] = None, end: Optional[date] = None). The calendar API can be filtered by these start/end dates. (alpaca.markets)

Examples

  1. Using datetime.date directly
from datetime import date
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import GetCalendarRequest

client = TradingClient(API_KEY, API_SECRET, paper=True)

req = GetCalendarRequest(start=date(2025, 8, 1), end=date(2025, 8, 15))
calendar = client.get_calendar(req)

for day in calendar:
    print(day)   # Calendar objects (has date, open, close, etc.)
  1. Converting from datetime.datetime
from datetime import datetime
# assume dt_start/dt_end are datetime objects
req = GetCalendarRequest(start=dt_start.date(), end=dt_end.date())
calendar = client.get_calendar(req)
  1. Converting from ISO string
from datetime import datetime
req = GetCalendarRequest(
    start=datetime.strptime("2025-08-01", "%Y-%m-%d").date(),
    end=datetime.strptime("2025-08-15", "%Y-%m-%d").date(),
)
calendar = client.get_calendar(req)

Other tips

  • start and end are date objects (no time component). (alpaca.markets)
  • If you omit both start and end you get the full calendar (docs note coverage from 1970 through 2029). (alpaca.markets)

If you want, tell me what form your dates are in (strings, datetimes, pandas timestamps) and I’ll give the exact conversion code you need.

Citations:


Convert start/end to date objects for GetCalendarRequest

GetCalendarRequest.start/end expect datetime.date (no time/tz). Passing timezone-aware datetimes risks subtle TZ/truncation issues — convert to .date().

  • File: applications/models/src/models/get_alpaca_calendar.py — where end/start are computed and passed to GetCalendarRequest.

Minimal fix (change only the request args):

GetCalendarRequest(
    start=start.date(),
    end=end.date(),
)

Or clearer refactor:

end_dt = datetime.now(tz=ZoneInfo("America/New_York"))
start_dt = end_dt - timedelta(days=365 * 6)
alpaca_client.get_calendar(
    GetCalendarRequest(start=start_dt.date(), end=end_dt.date())
)
🤖 Prompt for AI Agents
In applications/models/src/models/get_alpaca_calendar.py around lines 19 to 21,
the code computes timezone-aware datetimes for start/end and passes them to
GetCalendarRequest which expects datetime.date; update the call to pass dates
instead of datetimes by converting to .date() (minimal change: pass start.date()
and end.date() into GetCalendarRequest), or rename the variables to
end_dt/start_dt and call .date() when building the request to avoid
timezone/truncation issues.

Comment on lines +50 to +63
if self.dropout_rate > 0:
attention_weights = attention_weights.dropout(self.dropout_rate)

attention_output = attention_weights.matmul(value_weights)

attention_output = attention_output.transpose(1, 2).reshape(
batch_size, sequence_length, self.embedding_size
)

output = self.fully_connected_out(attention_output)

if self.dropout_rate > 0:
output = output.dropout(self.dropout_rate)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Dropout behavior: ensure it only applies during training

Confirm that tinygrad’s Tensor.dropout respects the global training flag. If not, gate dropout explicitly to avoid applying it during evaluation.

To verify, please check tinygrad’s dropout semantics and whether tests toggle training mode:


🏁 Script executed:

#!/bin/bash
# 1) Inspect usage of Tensor.dropout and training flag across the repo
rg -n -C2 'Tensor\.training|\.dropout\(' --type=py

# 2) Optionally, inspect tinygrad docs or source if vendored (if present in repo)
fd tinygrad | head -n 50

Length of output: 1396


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== mhsa_network.py (head) ==="
sed -n '1,240p' libraries/python/src/internal/mhsa_network.py || true
echo
echo "=== lstm_network.py (head) ==="
sed -n '1,240p' libraries/python/src/internal/lstm_network.py || true
echo
echo "=== Search for Tensor class or dropout implementation ==="
rg -n --hidden -S 'class\s+Tensor\b' --type=py || true
rg -n --hidden -S 'def\s+dropout\(' --type=py || true
echo
echo "=== Search for imports mentioning tinygrad ==="
rg -n --hidden -S '\btinygrad\b' --type=py || true
echo
echo "=== Search for .train() / .eval() / .training usage ==="
rg -n --hidden -S '\.train\(' --type=py || true
rg -n --hidden -S '\.eval\(' --type=py || true
rg -n --hidden -S '\.training\b' --type=py || true
echo
echo "=== Check repo dependency files for tinygrad (requirements/pyproject) ==="
rg -n --hidden -S 'tinygrad' -g '!**/node_modules/**' || true

Length of output: 10032


🌐 Web query:

tinygrad Tensor.dropout implementation

💡 Result:

Below is a compact, practical implementation of dropout you can drop into tinygrad's Tensor class. It uses "inverted dropout" (scale by 1/(1-p) at train time) so you don't need to change anything at eval time. The mask is a non-trainable Tensor so autograd just multiplies incoming gradients by the same mask during backward.

Notes:

  • This is a safe, non-destructive default (no inplace changes) — an optional inplace branch is included if you need it.
  • You can pass broadcast_dims to create a mask that broadcasts across some dimensions (useful for channel/spatial dropout).
  • If your tinygrad build uses different attribute names (e.g. .data vs .numpy()), adapt the small accesses accordingly.

Implementation (add to Tensor class):

import numpy as np
from tinygrad.tensor import Tensor  # adjust import path as needed

def dropout(self, p: float = 0.5, training: bool = True, inplace: bool = False,
            broadcast_dims: tuple = None, seed: int = None):
    """
    Inverted dropout:
    - p: dropout probability (0 <= p < 1)
    - training: if False, returns self unchanged
    - inplace: if True, tries to modify self in-place (may break autograd if used incorrectly)
    - broadcast_dims: tuple of dimension indices to broadcast mask across (e.g. (0,2,3) for channel-wise mask on NCHW)
    - seed: optional RNG seed for reproducibility
    """
    if not training or p == 0.0:
        return self
    if p < 0.0 or p >= 1.0:
        raise ValueError("p must be in [0, 1).")

    if seed is not None:
        rng = np.random.RandomState(seed)
        rand = rng.rand
    else:
        rand = np.random.rand

    # determine mask shape: either full shape or shape with ones for broadcast dims
    if broadcast_dims is None:
        mask_shape = tuple(self.shape)
    else:
        mask_shape = tuple(1 if i in broadcast_dims else s for i, s in enumerate(self.shape))

    # create mask, scale by 1/(1-p) for inverted dropout
    # use underlying dtype from tensor (adjust if your API differs)
    dtype = getattr(self, "data", None)
    if dtype is not None:
        npdtype = self.data.dtype
    else:
        # fallback if .data not present
        npdtype = self.numpy().dtype

    mask_np = (rand(*mask_shape) > p).astype(npdtype) / (1.0 - p)
    mask_t = Tensor(mask_np, requires_grad=False)

    if inplace:
        # attempt in-place multiplication (modify internal storage)
        # Make sure your tinygrad supports .data modification safely
        self.data *= mask_t.data
        return self
    else:
        return self * mask_t

Example usage:

x = Tensor(np.ones((4, 3, 8, 8)), requires_grad=True)  # sample NCHW
y = x.dropout(p=0.2, training=True)                    # elementwise dropout
# channel-wise dropout (same mask for every spatial location and batch):
y_ch = x.dropout(p=0.2, training=True, broadcast_dims=(0,2,3))

Why this works with autograd:

  • mask_t is created with requires_grad=False so it is treated as a constant; the forward multiplies x by mask_t.
  • during backward, gradients are multiplied by the same mask (and scaled), which implements dropout's gradient behavior.

Small test (sanity):

import numpy as np
x = Tensor(np.ones((10000,)), requires_grad=True)
y = x.dropout(p=0.5, training=True, seed=42)
print(y.numpy().mean())  # should be close to 1.0 because of inverted scaling

If you want the exact tinygrad integration (e.g. to use tinygrad RNG ops instead of numpy or to match your library internals) paste your current Tensor class here and I can adapt the patch precisely.


Dropout must be gated to run only in training mode (fix required)

MHSA currently calls Tensor.dropout unconditionally; ensure dropout runs only during training (either pass a training flag into forward and to Tensor.dropout, or conditionally call dropout). LSTM calls .train() on the hidden state before dropout, so follow a consistent, explicit pattern.

Files to change:

  • libraries/python/src/internal/mhsa_network.py — forward (attention_weights and output dropout calls, ~lines 49–63)
  • Update callers/tests (e.g. libraries/python/tests/test_mhsa_network.py) to pass the training flag or use model.train()/eval() as appropriate.

Suggested change (minimal example — prefer explicit training arg):

Replace forward signature and the unconditional drops:

from

def forward(self, inputs: Tensor) -> tuple[Tensor, Tensor]:
    ...
    attention_weights = attention_scores.softmax(axis=-1)

    if self.dropout_rate > 0:
        attention_weights = attention_weights.dropout(self.dropout_rate)
    ...
    if self.dropout_rate > 0:
        output = output.dropout(self.dropout_rate)
    return output, attention_weights

with

def forward(self, inputs: Tensor, training: bool = True) -> tuple[Tensor, Tensor]:
    ...
    attention_weights = attention_scores.softmax(axis=-1)

    if self.dropout_rate > 0 and training:
        attention_weights = attention_weights.dropout(self.dropout_rate)
    ...
    if self.dropout_rate > 0 and training:
        output = output.dropout(self.dropout_rate)
    return output, attention_weights

Or, if tinygrad's Tensor.dropout supports a training kwarg, use:

attention_weights = attention_weights.dropout(self.dropout_rate, training=training)
output = output.dropout(self.dropout_rate, training=training)

Remember to update all call sites/tests to pass the training flag (or call the appropriate .train()/.eval() API) so inference doesn't apply dropout.

🤖 Prompt for AI Agents
In libraries/python/src/internal/mhsa_network.py around lines 50 to 63, dropout
is applied unconditionally to attention_weights and output; change the forward
signature to accept a training: bool = True (or use Tensor.dropout's training
kwarg if available) and guard both dropout calls so they only run when
self.dropout_rate > 0 and training is True (or pass training into dropout).
Update all callers and tests (e.g., libraries/python/tests/test_mhsa_network.py)
to pass the training flag or switch to model.train()/eval() so inference does
not apply dropout.

@forstmeier forstmeier closed this Aug 15, 2025
@forstmeier
Copy link
Copy Markdown
Collaborator Author

Closing because it's gotten too convoluted with review feedback - opening a fresh one to do reviews there.

@forstmeier forstmeier deleted the 07-28-restructure_python_application_and_library_resources branch August 15, 2025 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants