Skip to content

PnL Engine + Tests + CI/CD - #3

Merged
inditilve merged 18 commits into
masterfrom
feat/pnl-engine
Jan 19, 2026
Merged

inditilve merged 18 commits into
masterfrom
feat/pnl-engine

Conversation

@inditilve

@inditilve inditilve commented Jan 14, 2026 •

Copy link
Copy Markdown
Owner

Basic PnL Engine

Summary by CodeRabbit

  • New Features

    • Added a real-time profit/loss calculation engine supporting position tracking, long/short positions, realized and unrealized PnL attribution, and multi-symbol portfolio management.
  • Tests

    • Added comprehensive unit tests for PnL engine functionality.
  • Chores

    • Configured continuous integration pipeline with automated linting, type checking, and test execution.
    • Updated project dependencies and development environment configuration.

✏️ Tip: You can customize this high-level summary in your review settings.


Open with Devin

@inditilve inditilve self-assigned this Jan 14, 2026
@inditilve
inditilve marked this pull request as draft January 14, 2026 09:38
@github-actions

github-actions Bot commented Jan 19, 2026 •

Copy link
Copy Markdown

Test Results

26 tests   26 ✅  0s ⏱️
 1 suites   0 💤
 1 files     0 ❌

Results for commit e8a4ecc.

♻️ This comment has been updated with latest results.

@coderabbitai

coderabbitai Bot commented Jan 19, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request establishes a complete trading system foundation by introducing CI/CD workflows, project infrastructure (pyproject.toml, requirements.txt, .gitignore, pre-commit hooks, environment configuration), domain models (Trade, Position, Side), a RealTimePnLEngine for real-time profit/loss attribution, and comprehensive unit tests validating the engine's trade processing, position tracking, and PnL calculations.

Changes

Cohort / File(s) Summary
CI/CD Workflow
.github/workflows/ci.yml
Adds GitHub Actions workflow named "Build, Lint, and Run Tests" that triggers on pull requests to master. Defines checkout, Python 3.12 setup, dependency installation, linting via ruff (check and format), type checking via mypy, pytest execution with junit XML and HTML coverage reports, and artifact uploads.
Project Configuration
.gitignore, .pre-commit-config.yaml, pyproject.toml, requirements.txt, environment.yml
Expands .gitignore with Python testing, type checking, IDE, and environment directories. Adds pre-commit hooks for ruff (v0.14.13) and mypy (v1.7.1). Configures pyproject.toml with build system, project metadata, ruff linting rules, mypy settings, and pytest markers. Updates requirements.txt with pytest-html, ruff, mypy, pre-commit. Adds Conda environment configuration.
Core Package Initialization
core/__init__.py
Adds package docstring for core trading modules.
PnL Engine Implementation
core/pnl_engine.py
Implements RealTimePnLEngine class with 14 methods for real-time PnL attribution. Handles trade validation and routing (same/opposite-direction), tracks position quantities and average costs, computes realized/unrealized PnL, manages price updates, and provides position filtering and summary logging.
Domain Models
models/__init__.py, models/domain.py
Introduces domain module with generate_id(), Side enum (BUY/SELL), Trade dataclass (symbol, side, qty, price, with notional\_value and signed\_qty methods), and Position dataclass (account\_id, symbol, qty, avg\_cost, with methods for state checks and PnL calculations). Re-exports all three public entities in models/__init__.py.
Test Scaffolding
tests/conftest.py, tests/unit/__init__.py, tests/unit/test_pnl_engine.py
Adds test package marker in tests/unit/__init__.py. Defines sample_trade fixture in conftest.py. Introduces comprehensive unit test suite (334 lines) validating engine initialization, trade processing, position updates, same/opposite-direction trades, price updates, PnL attribution, multi-symbol independence, and filtering helpers.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Engine as RealTimePnLEngine
    participant TradeHandler as Trade Handler
    participant PosManager as Position Manager
    participant PnLTracker as PnL Tracker

    Client->>Engine: on_trade(trade)
    activate Engine
    Engine->>Engine: validate trade (qty>0, price>0)
    Engine->>PosManager: get_position(symbol)
    activate PosManager
    PosManager-->>Engine: position
    deactivate PosManager

    alt Same Direction
        Engine->>TradeHandler: _handle_same_direction_trade(pos, qty, price)
        activate TradeHandler
        TradeHandler->>PosManager: update avg_cost (weighted avg)
        TradeHandler->>PosManager: update qty (increase position)
        TradeHandler-->>Engine: position updated
        deactivate TradeHandler
    else Opposite Direction
        Engine->>TradeHandler: _handle_opposite_direction_trade(pos, qty, price)
        activate TradeHandler
        TradeHandler->>PnLTracker: _realize_pnl(symbol, closing_qty, price)
        activate PnLTracker
        PnLTracker->>PnLTracker: calculate realized PnL
        PnLTracker->>PnLTracker: accumulate per-symbol realized PnL
        PnLTracker-->>TradeHandler: pnl computed
        deactivate PnLTracker
        TradeHandler->>PosManager: update position (close or flip)
        TradeHandler-->>Engine: position settled
        deactivate TradeHandler
    end

    deactivate Engine

    Client->>Engine: on_price(symbol, price)
    activate Engine
    Engine->>Engine: store latest_prices[symbol]
    deactivate Engine

    Client->>Engine: get_total_pnl()
    activate Engine
    Engine->>PnLTracker: sum realized_pnl across all symbols
    Engine->>Engine: compute unrealized PnL for each position
    Engine-->>Client: realized + unrealized total
    deactivate Engine
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes


🐰 A new trading engine hops into place,
With positions tracked at rapid pace,
PnL counted both real and not,
Tests ensure each calculation's hot,
This rabbit cheers the system's embrace! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'PnL Engine' is vague and generic, using a non-descriptive term that doesn't convey the specific implementation details or context of the changeset. Consider a more descriptive title that clarifies what aspect of the PnL engine is being introduced, such as 'Add RealTimePnLEngine class for real-time P&L attribution' or 'Implement core PnL engine with position tracking'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@inditilve inditilve linked an issue Jan 19, 2026 that may be closed by this pull request
@inditilve
inditilve marked this pull request as ready for review January 19, 2026 13:42
@inditilve

Copy link
Copy Markdown
Owner Author

@CodeRabbit help

@coderabbitai

coderabbitai Bot commented Jan 19, 2026

Copy link
Copy Markdown

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.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
    • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
    • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit 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.
  • You can also validate your configuration using the online YAML validator.
  • 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, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@core/pnl_engine.py`:
- Around line 112-121: The None check in get_unrealized_pnl is dead because
last_prices is a defaultdict(float); either switch last_prices to a plain dict
in __init__ (self.last_prices: dict[str, float] = {}) so last_prices.get(symbol)
can return None and the existing None-check in get_unrealized_pnl remains
correct, or keep defaultdict(float) and remove the unreachable "if last_px is
None" branch and treat 0.0 as the no-price sentinel; update the code paths
referencing last_prices, including get_unrealized_pnl and any other usage, to
follow the chosen approach.

In `@models/domain.py`:
- Line 44: The Position.updated_at field uses
default_factory=datetime.now().astimezone which evaluates once and is shared
across instances; change it to a callable that returns a fresh timestamp each
instantiation (e.g., use a lambda that calls datetime.now().astimezone()) so
Position.updated_at produces a new datetime per instance—update the Position
dataclass field definition accordingly (same fix pattern as for
Trade.timestamp).
- Line 27: The Trade dataclass's timestamp field uses
default_factory=datetime.now().astimezone which is evaluated at import time;
change the default_factory to a zero-arg callable that produces a fresh
timezone-aware datetime for each instance (e.g. use a lambda or function that
returns datetime.now().astimezone()). Update the timestamp field (symbol:
timestamp in the Trade dataclass) to use that callable so each new Trade gets
the current time when constructed.

In `@requirements.txt`:
- Line 14: requirements.txt pins ruff to 0.1.11 while .pre-commit-config.yaml
uses v0.14.13, causing inconsistent linting; pick one canonical version and make
both files match (preferably update requirements.txt to ruff==0.14.13 to match
.pre-commit-config.yaml or update .pre-commit-config.yaml to v0.1.11 if you must
keep the older version), then run the linter locally to verify no
version-specific failures; ensure the version string "ruff==0.1.11" or
"v0.14.13" is updated accordingly in requirements.txt and
.pre-commit-config.yaml so both reference the same ruff release.
- Line 11: Requirements pin for FastAPI is vulnerable; update the fastapi
package entry in requirements.txt from fastapi[all]==0.104.1 to a secure version
(e.g., fastapi[all]>=0.109.1) to remediate PYSEC-2024-38 / CVE-2024-24762,
keeping the extras ([all]) intact and ensuring any dependency resolution or
lockfile regeneration is performed after the change.

In `@tests/conftest.py`:
- Around line 1-20: Remove the unused pytest fixture named mock_logger from
tests/conftest.py: locate the fixture definition "def mock_logger() ->
MagicMock" and delete that whole fixture so only the used fixtures sample_trade
and sample_position remain; ensure imports still needed (remove MagicMock import
if no longer used) to keep the file clean.

In `@tests/unit/test_pnl_engine.py`:
- Around line 62-64: The test's weighted average cost assertion is dividing by
sample_trade.price instead of the total quantity; update the assertion to divide
the combined notional by the total quantity (e.g. use pos.qty or the explicit
total 150) so avg_cost is computed as (sample_trade.notional_value() + 50 *
160.0) / pos.qty; locate this in the test where pos: Position =
engine.get_position("AAPL") and replace the incorrect denominator
sample_trade.price with pos.qty (or 150).
🧹 Nitpick comments (7)
pyproject.toml (1)

47-50: Consolidate pytest marker definitions: acceptance marker missing in pyproject.toml.

pytest.ini defines markers unit, integration, acceptance, and slow, but pyproject.toml only defines unit, integration, and slow. The missing acceptance marker will cause inconsistency when marker definitions are split across files. Move all marker definitions to a single location to ensure consistency and prevent drift.

.github/workflows/ci.yml (1)

3-5: Use list syntax for branches.

For consistency with GitHub Actions conventions and to avoid potential parsing issues, use the list syntax for the branches filter.

Suggested fix
 on:
   pull_request:
-    branches: master
+    branches: [master]
.gitignore (1)

56-57: Minor: Add space after # in comment.

For consistency with other comments in the file.

-#Dev Notes
+# Dev Notes
 notes/
core/pnl_engine.py (2)

14-16: Consider using a regular dict instead of defaultdict for positions.

The defaultdict creates a Position with symbol="" for missing keys, which could lead to confusing behavior. Since on_trade explicitly checks and creates positions with the correct symbol (lines 39-40), the default factory is never used properly. A regular dict with explicit key checks would be clearer.

Suggested simplification
-        self.positions: dict[str, Position] = defaultdict(
-            lambda: Position(account_id=self.account_id, symbol="")
-        )
+        self.positions: dict[str, Position] = {}

88-95: Type hints inconsistent with actual usage.

Parameters closing_qty, prev_cost, and prev_qty are typed as float, but qty in Trade and Position is int. Consider using int for closing_qty and prev_qty to match the domain model.

Suggested fix
     def _realize_pnl(
         self,
         symbol: str,
-        closing_qty: float,
+        closing_qty: int,
         trade_price: float,
         prev_cost: float,
-        prev_qty: float,
+        prev_qty: int,
     ):
models/domain.py (1)

11-13: Side enum is defined but unused.

The Side enum is not used anywhere in this codebase. Either remove it if not needed, or add it to the public exports in models/__init__.py if it's intended for future use.

tests/unit/test_pnl_engine.py (1)

15-15: Consider using comments instead of standalone string literals for section headers.

Lines 15, 51, 93, 148, 178, 213, 256, and 289 use bare string literals as section headers. While this works (they're no-ops), conventional Python style uses # comments for inline documentation that isn't a docstring.

-    """Basic Tests"""
+    # Basic Tests

Comment thread core/pnl_engine.py
Comment thread models/domain.py Outdated
Comment thread models/domain.py Outdated
Comment thread requirements.txt Outdated
Comment thread requirements.txt Outdated
Comment thread tests/conftest.py Outdated
Comment thread tests/unit/test_pnl_engine.py Outdated
inditilve and others added 3 commits January 19, 2026 22:02
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@tests/unit/test_pnl_engine.py`:
- Around line 1-330: The file tests/unit/test_pnl_engine.py fails the Ruff
formatting check; run the formatter (e.g., ruff format
tests/unit/test_pnl_engine.py) to reformat the file and stage the changes before
pushing; ensure the test class TestRealTimePnLEngine and its test_* functions
(e.g., test_initial_state_empty, test_on_trade_buy_creates_position,
test_get_total_pnl_combines_realized_and_unrealized) remain unchanged in
behavior after formatting.
- Around line 156-157: The inline comment after engine.on_trade(trade_sell_more)
is incorrect: it says "Sell 150@140" while the trade object trade_sell_more was
created with price=160.0; update the comment to reflect the actual trade (e.g.,
"Sell 150@160") or change the trade_sell_more creation to price=140.0 to match
the comment—locate the usage at engine.on_trade(trade_sell_more) and the trade
definition for trade_sell_more to keep the test and comment consistent.
🧹 Nitpick comments (1)
tests/unit/test_pnl_engine.py (1)

15-15: Standalone string literals are no-ops; use comments for section headers.

"""Basic Tests""" and similar strings at lines 51, 93, 148, 178, 213, 256, 289 are not docstrings (they're not immediately following a class/function definition). They're compiled as unused expressions.

♻️ Suggested fix
-    """Basic Tests"""
+    # --- Basic Tests ---

Apply the same pattern to all section headers throughout the file.

Comment thread tests/unit/test_pnl_engine.py
Comment thread tests/unit/test_pnl_engine.py Outdated
…f positive/negative qty; minor nits; test updates
@inditilve inditilve changed the title PnL Engine PnL Engine @coderabbitai Jan 19, 2026
@inditilve inditilve changed the title PnL Engine @coderabbitai PnL Engine + Tests + CI/CD Jan 19, 2026
@inditilve
inditilve merged commit c3e7d97 into master Jan 19, 2026
3 checks passed
@inditilve
inditilve deleted the feat/pnl-engine branch January 19, 2026 14:44

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View issue and 5 additional flags in Devin Review.

Open in Devin Review

Comment thread core/pnl_engine.py
Comment on lines +126 to +127
last_px = self.last_prices[symbol]
return pos.unrealized_pnl(last_px)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Unrealized PnL calculated with price=0 when no market price has been set

When get_unrealized_pnl is called for a symbol that has a position but no price has been set via on_price, it silently uses 0.0 as the price due to last_prices being a defaultdict(float). This leads to wildly incorrect PnL calculations.

Click to expand

How the bug is triggered

  1. A trade is processed via on_trade(), creating a position
  2. get_unrealized_pnl() is called before on_price() sets the market price
  3. At line 126, last_px = self.last_prices[symbol] returns 0.0 (defaultdict default)
  4. The unrealized PnL is calculated as qty * (0.0 - avg_cost) = -qty * avg_cost

Example

engine = RealTimePnLEngine()
engine.on_trade(Trade(symbol='AAPL', side=Side.BUY, qty=100, price=150.0))
unrealized = engine.get_unrealized_pnl('AAPL')  # Returns -15000.0 instead of 0 or error

Impact

  • get_unrealized_pnl() returns incorrect values
  • get_total_pnl() at line 131 returns incorrect totals
  • get_pnl_by_symbol() at line 149 returns incorrect unrealized/total
  • log_summary() at line 164 displays incorrect unrealized PnL

For a long position bought at $150, the unrealized PnL would be reported as -$15,000 (a massive loss) when no price update has been received, rather than indicating that the price is unknown.

Recommendation: Either check if the symbol exists in last_prices and raise an error or return 0/None when the price is missing, or use a regular dict instead of defaultdict for last_prices and handle KeyError appropriately. For example:

def get_unrealized_pnl(self, symbol: str) -> float:
    pos = self.get_position(symbol)
    if pos.qty == 0:
        return 0.0
    if symbol not in self.last_prices:
        raise ValueError(f"No price available for {symbol}")
    return pos.unrealized_pnl(self.last_prices[symbol])
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@mesa-dot-dev mesa-dot-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Performed full review of 6a5bb99...e8a4ecc

Analysis

  1. Precision Issues (CRITICAL): Using Python float for financial calculations will accumulate precision errors over time. Must switch to Decimal type for all monetary values before production use.

  2. Concurrency & Thread Safety (CRITICAL): The engine maintains mutable shared state without any synchronization mechanisms. This will lead to race conditions and data corruption in concurrent environments. Needs locks, actor model, or immutable approach.

  3. Memory Management Problems: Unbounded memory growth as positions are never removed and price history grows indefinitely. Implement cleanup for closed positions and stale data.

  4. Persistence & Resilience Gaps: No state persistence or crash recovery means all positions and PnL are lost on restart. Missing event sourcing for audit trails and replay capability.

  5. Error Handling Weaknesses: Silent failures instead of fail-fast approach, with KeyError risks when prices are missing for unrealized PnL calculation. Needs explicit error handling with proper exceptions.

Tip

Help

Slash Commands:

  • /review - Request a full code review
  • /review latest - Review only changes since the last review
  • /describe - Generate PR description. This will update the PR body or issue comment depending on your configuration
  • /help - Get help with Mesa commands and configuration options

13 files reviewed | 4 comments | Edit Agent Settings • Read Docs

Comment thread core/pnl_engine.py

def __init__(self, account_id: str = "system"):
self.account_id = account_id
self.positions: dict[str, Position] = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High

This PnL engine lacks thread safety mechanisms, making it unsafe for concurrent access in real-time trading systems where multiple threads may process trades simultaneously. Consider adding thread synchronization:

from threading import RLock

class RealTimePnLEngine:
    def __init__(self, account_id: str = "system"):
        self._lock = RLock()
        # ...
    
    def on_trade(self, trade: Trade) -> None:
        with self._lock:
            # existing logic

Without this, race conditions can corrupt position state and PnL calculations.

Agent: 🏛 Architecture • Fix in Cursor • Fix in Claude

Prompt for Agent
Task: Address review feedback left on GitHub.
Repository: inditilve/trading-primitives#3
File: core/pnl_engine.py#L14
Action: Open this file location in your editor, inspect the highlighted code, and resolve the issue described below.

Feedback:
This PnL engine lacks thread safety mechanisms, making it unsafe for concurrent access in real-time trading systems where multiple threads may process trades simultaneously. Consider adding thread synchronization:

```python
from threading import RLock

class RealTimePnLEngine:
    def __init__(self, account_id: str = "system"):
        self._lock = RLock()
        # ...
    
    def on_trade(self, trade: Trade) -> None:
        with self._lock:
            # existing logic

Without this, race conditions can corrupt position state and PnL calculations.


</details>

Comment thread models/domain.py
symbol: str
side: Side
qty: int # Always positive; side determines direction
price: float

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High

Using float for financial calculations introduces precision errors that compound over time. For a PnL engine handling real money, this can lead to significant discrepancies. Consider using Decimal from the decimal module instead:

from decimal import Decimal

@dataclass
class Trade:
    price: Decimal
    # ...

This is critical for production financial systems where rounding errors of even fractions of a cent can accumulate to material amounts.

Agent: 🏛 Architecture • Fix in Cursor • Fix in Claude

Prompt for Agent
Task: Address review feedback left on GitHub.
Repository: inditilve/trading-primitives#3
File: models/domain.py#L25
Action: Open this file location in your editor, inspect the highlighted code, and resolve the issue described below.

Feedback:
Using `float` for financial calculations introduces precision errors that compound over time. For a PnL engine handling real money, this can lead to significant discrepancies. Consider using `Decimal` from the `decimal` module instead:

```python
from decimal import Decimal

@dataclass
class Trade:
    price: Decimal
    # ...

This is critical for production financial systems where rounding errors of even fractions of a cent can accumulate to material amounts.


</details>

Comment thread core/pnl_engine.py

def __init__(self, account_id: str = "system"):
self.account_id = account_id
self.positions: dict[str, Position] = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium

The positions dict will grow unbounded as symbols are traded, even after positions are fully closed (qty=0). This creates a memory leak over time. Consider implementing a cleanup mechanism:

def _cleanup_closed_positions(self) -> None:
    """Remove positions with zero qty to prevent memory bloat"""
    self.positions = {sym: pos for sym, pos in self.positions.items() if pos.qty != 0}

Call this periodically or after position closes. For a long-running system, this can become significant.

Agent: 🏛 Architecture • Fix in Cursor • Fix in Claude

Prompt for Agent
Task: Address review feedback left on GitHub.
Repository: inditilve/trading-primitives#3
File: core/pnl_engine.py#L14
Action: Open this file location in your editor, inspect the highlighted code, and resolve the issue described below.

Feedback:
The `positions` dict will grow unbounded as symbols are traded, even after positions are fully closed (qty=0). This creates a memory leak over time. Consider implementing a cleanup mechanism:

```python
def _cleanup_closed_positions(self) -> None:
    """Remove positions with zero qty to prevent memory bloat"""
    self.positions = {sym: pos for sym, pos in self.positions.items() if pos.qty != 0}

Call this periodically or after position closes. For a long-running system, this can become significant.


</details>

Comment thread core/pnl_engine.py
"""Update position and realized PnL"""

# Validate trade
if trade.qty <= 0:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium

The validation silently ignores invalid trades with only a warning. For a financial system, invalid trades should either raise an exception or return a validation result so the caller can handle the error appropriately. Consider:

def on_trade(self, trade: Trade) -> bool:
    """Update position and realized PnL. Returns True if trade was processed."""
    if trade.qty <= 0:
        raise ValueError(f"Invalid trade {trade.trade_id}: qty must be positive")
    if trade.price < 0:
        raise ValueError(f"Invalid trade {trade.trade_id}: price cannot be negative")
    # ... rest of logic
    return True

This allows upstream systems to detect and handle validation failures.

Agent: 🏛 Architecture • Fix in Cursor • Fix in Claude

Prompt for Agent
Task: Address review feedback left on GitHub.
Repository: inditilve/trading-primitives#3
File: core/pnl_engine.py#L23
Action: Open this file location in your editor, inspect the highlighted code, and resolve the issue described below.

Feedback:
The validation silently ignores invalid trades with only a warning. For a financial system, invalid trades should either raise an exception or return a validation result so the caller can handle the error appropriately. Consider:

```python
def on_trade(self, trade: Trade) -> bool:
    """Update position and realized PnL. Returns True if trade was processed."""
    if trade.qty <= 0:
        raise ValueError(f"Invalid trade {trade.trade_id}: qty must be positive")
    if trade.price < 0:
        raise ValueError(f"Invalid trade {trade.trade_id}: price cannot be negative")
    # ... rest of logic
    return True

This allows upstream systems to detect and handle validation failures.


</details>

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.

Basic PnL Engine

1 participant