From 570c27c6ba56b6925d304153e7de566080b6f189 Mon Sep 17 00:00:00 2001 From: agent-14fd4b738639d573 Date: Fri, 24 Apr 2026 21:05:42 +0200 Subject: [PATCH] build(agent): melter#14fd4b iteration --- .gitignore | 21 +++++++ AGENTS.md | 32 ++++++++++ README.md | 16 ++++- .../__init__.py | 18 ++++++ .../adapters/csv_adapter.py | 26 ++++++++ .../adapters/custodian_adapter.py | 16 +++++ idea115_taxalpha_studio_declarative/dsl.py | 63 +++++++++++++++++++ idea115_taxalpha_studio_declarative/ir.py | 39 ++++++++++++ idea115_taxalpha_studio_declarative/solver.py | 61 ++++++++++++++++++ pyproject.toml | 15 +++++ test.sh | 13 ++++ tests/test_solver.py | 16 +++++ 12 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 idea115_taxalpha_studio_declarative/__init__.py create mode 100644 idea115_taxalpha_studio_declarative/adapters/csv_adapter.py create mode 100644 idea115_taxalpha_studio_declarative/adapters/custodian_adapter.py create mode 100644 idea115_taxalpha_studio_declarative/dsl.py create mode 100644 idea115_taxalpha_studio_declarative/ir.py create mode 100644 idea115_taxalpha_studio_declarative/solver.py create mode 100644 pyproject.toml create mode 100755 test.sh create mode 100644 tests/test_solver.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +node_modules/ +.npmrc +.env +.env.* +__tests__/ +coverage/ +.nyc_output/ +dist/ +build/ +.cache/ +*.log +.DS_Store +tmp/ +.tmp/ +__pycache__/ +*.pyc +.venv/ +venv/ +*.egg-info/ +.pytest_cache/ +READY_TO_PUBLISH diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e6629b3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +Architecture and contributor guidance for idea115-taxalpha-studio-declarative + +Overview +- Phase-0 skeleton for TaxAlpha Studio: canonical IR, a tiny DSL parser, + a deterministic greedy optimizer (warm-start), and two simple adapters + (custodian mock and CSV reader). + +Tech stack +- Python 3.8+ +- Packaging: setuptools (pyproject.toml) +- Testing: pytest + +Repository layout +- idea115_taxalpha_studio_declarative/: package source + - ir.py: canonical dataclasses (TaxLot, Account, HarvestAction, PlanDelta, AuditLog) + - dsl.py: very small line-oriented DSL parser + - solver.py: deterministic greedy warm-start optimizer (Phase-0) + - adapters/: simple adapters (custodian mock, CSV reader) +- tests/: pytest tests +- pyproject.toml: package metadata and build-system +- README.md: project description +- AGENTS.md: this file + +Developer rules +- Make minimal, well-scoped changes. Prefer small, correct edits. +- Use apply_patch for modifications. +- Add tests for new behavior and ensure pytest passes. +- test.sh must run `pytest` and `python3 -m build` successfully. + +How to run tests +1. Install requirements if needed (Phase-0 uses stdlib only) +2. Run: `bash test.sh` diff --git a/README.md b/README.md index 591ed2a..d88e508 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ # idea115-taxalpha-studio-declarative -Source logic for Idea #115 \ No newline at end of file +TaxAlpha Studio (Phase-0 skeleton) + +This repository contains a Phase-0 skeleton for the TaxAlpha Studio project +— a declarative, auditable tax-aware harvesting and optimization engine. + +What is included +- A tiny canonical IR (TaxLot, Account, HarvestAction, PlanDelta, AuditLog) +- A line-oriented DSL parser (very small, deterministic) +- A deterministic greedy warm-start optimizer (solver prototype) +- Two simple adapters: CSV reader and a mocked custodian adapter +- Tests and packaging metadata so the project builds and tests locally + +Run tests + +bash test.sh diff --git a/idea115_taxalpha_studio_declarative/__init__.py b/idea115_taxalpha_studio_declarative/__init__.py new file mode 100644 index 0000000..4ea4bb0 --- /dev/null +++ b/idea115_taxalpha_studio_declarative/__init__.py @@ -0,0 +1,18 @@ +"""idea115_taxalpha_studio_declarative + +Minimal Phase-0 package for TaxAlpha Studio. +Provides a tiny DSL -> IR, a greedy solver warm-start, and simple adapters. +""" +from .ir import TaxLot, Account, HarvestAction, PlanDelta, AuditLog +from .dsl import parse_declarations +from .solver import optimize_harvest + +__all__ = [ + "TaxLot", + "Account", + "HarvestAction", + "PlanDelta", + "AuditLog", + "parse_declarations", + "optimize_harvest", +] diff --git a/idea115_taxalpha_studio_declarative/adapters/csv_adapter.py b/idea115_taxalpha_studio_declarative/adapters/csv_adapter.py new file mode 100644 index 0000000..032e211 --- /dev/null +++ b/idea115_taxalpha_studio_declarative/adapters/csv_adapter.py @@ -0,0 +1,26 @@ +import csv +from datetime import datetime +from typing import List +from ..ir import TaxLot + + +def read_taxlots_from_csv(path: str) -> List[TaxLot]: + """Read a simple CSV with headers: id,acquisition_date,basis,quantity,market_value + + Returns list of TaxLot. This adapter is intentionally minimal for Phase-0. + """ + lots = [] + with open(path, newline="") as f: + rdr = csv.DictReader(f) + for row in rdr: + acq = row.get("acquisition_date") + acq_date = datetime.strptime(acq, "%Y-%m-%d").date() if acq else None + lot = TaxLot( + id=row.get("id"), + acquisition_date=acq_date, + basis=float(row.get("basis", 0)), + quantity=float(row.get("quantity", 0)), + market_value=float(row.get("market_value")) if row.get("market_value") else None, + ) + lots.append(lot) + return lots diff --git a/idea115_taxalpha_studio_declarative/adapters/custodian_adapter.py b/idea115_taxalpha_studio_declarative/adapters/custodian_adapter.py new file mode 100644 index 0000000..734eec1 --- /dev/null +++ b/idea115_taxalpha_studio_declarative/adapters/custodian_adapter.py @@ -0,0 +1,16 @@ +from datetime import date +from typing import List +from ..ir import TaxLot + + +def fetch_mock_custodian_lots() -> List[TaxLot]: + """Return a small deterministic set of lots for integration testing. + + In a real adapter this would call custodian APIs and map responses to the + canonical IR. For Phase-0 we provide a mocked deterministic dataset. + """ + return [ + TaxLot(id="lotA", acquisition_date=date(2019, 6, 1), basis=100.0, quantity=10, market_value=80.0), + TaxLot(id="lotB", acquisition_date=date(2020, 1, 15), basis=50.0, quantity=5, market_value=70.0), + TaxLot(id="lotC", acquisition_date=date(2018, 3, 3), basis=200.0, quantity=2, market_value=150.0), + ] diff --git a/idea115_taxalpha_studio_declarative/dsl.py b/idea115_taxalpha_studio_declarative/dsl.py new file mode 100644 index 0000000..f782e6b --- /dev/null +++ b/idea115_taxalpha_studio_declarative/dsl.py @@ -0,0 +1,63 @@ +"""Tiny DSL parser for Phase-0. + +Supports simple line-oriented declarations like: + +TaxLot id=lot1 acquisition_date=2020-01-01 basis=100 quantity=10 market_value=80 +Account id=acct1 jurisdiction=US tax_profile=taxable + +This is intentionally minimal and deterministic for testing and the early +prototype. It returns IR dataclasses. +""" +from .ir import TaxLot, Account +from datetime import datetime +from typing import List + + +def _parse_kv_tokens(tokens): + data = {} + for t in tokens: + if "=" not in t: + continue + k, v = t.split("=", 1) + data[k.strip()] = v.strip() + return data + + +def parse_declarations(text: str): + """Parse multiple lines of simple declarations into IR objects. + + Returns dict with lists: {"taxlots": [...], "accounts": [...]}. + """ + taxlots = [] + accounts = [] + for raw in text.splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + kind = parts[0] + kv = _parse_kv_tokens(parts[1:]) + if kind == "TaxLot": + acq = kv.get("acquisition_date") + acq_date = datetime.strptime(acq, "%Y-%m-%d").date() if acq else None + lot = TaxLot( + id=kv.get("id"), + acquisition_date=acq_date, + basis=float(kv.get("basis", 0)), + quantity=float(kv.get("quantity", 0)), + market_value=float(kv.get("market_value")) if kv.get("market_value") else None, + lot_status=kv.get("lot_status", "open"), + ) + taxlots.append(lot) + elif kind == "Account": + acct = Account( + id=kv.get("id"), + jurisdiction=kv.get("jurisdiction", "unknown"), + tax_profile=kv.get("tax_profile", "unknown"), + ) + accounts.append(acct) + else: + # unknown declarations ignored for now + continue + + return {"taxlots": taxlots, "accounts": accounts} diff --git a/idea115_taxalpha_studio_declarative/ir.py b/idea115_taxalpha_studio_declarative/ir.py new file mode 100644 index 0000000..7f36a18 --- /dev/null +++ b/idea115_taxalpha_studio_declarative/ir.py @@ -0,0 +1,39 @@ +from dataclasses import dataclass, field +from datetime import date +from typing import Optional, List, Any + + +@dataclass +class TaxLot: + id: str + acquisition_date: date + basis: float + quantity: float + market_value: Optional[float] = None + lot_status: str = "open" + + +@dataclass +class Account: + id: str + jurisdiction: str + tax_profile: str + + +@dataclass +class HarvestAction: + lot_id: str + sell_qty: float + date: date + expected_gain: float + + +@dataclass +class PlanDelta: + actions: List[HarvestAction] = field(default_factory=list) + metadata: dict = field(default_factory=dict) + + +@dataclass +class AuditLog: + entries: List[Any] = field(default_factory=list) diff --git a/idea115_taxalpha_studio_declarative/solver.py b/idea115_taxalpha_studio_declarative/solver.py new file mode 100644 index 0000000..028dfe8 --- /dev/null +++ b/idea115_taxalpha_studio_declarative/solver.py @@ -0,0 +1,61 @@ +"""Very small solver prototype. + +Provides a deterministic greedy warm-start optimizer for harvesting losses. +For Phase-0 we avoid external MILP dependencies and implement a clear, +deterministic algorithm suitable for unit testing. Future versions will +replace or augment this with MILP (e.g., using pulp or ortools). +""" +from datetime import date +from typing import List, Tuple +from .ir import TaxLot, HarvestAction, PlanDelta, AuditLog + + +def _lot_loss(lot: TaxLot) -> float: + """Return (market_value - basis) * quantity. Positive=gain, negative=loss. + + Note: if market_value is None treat as 0 to keep deterministic behavior. + """ + mv = lot.market_value if lot.market_value is not None else 0.0 + return (mv - lot.basis) * lot.quantity + + +def optimize_harvest(lots: List[TaxLot], target_loss: float) -> Tuple[PlanDelta, AuditLog]: + """Greedy harvest: choose lots with largest losses first until reaching + target_loss (in absolute terms). target_loss should be positive number + indicating total loss to realize (e.g., harvest $10,000 of losses). + + Returns a PlanDelta and AuditLog. + """ + # Work only with lots that currently show a loss + losses = [] + for l in lots: + loss = -_lot_loss(l) if _lot_loss(l) < 0 else 0.0 + if loss > 0: + losses.append((loss, l)) + + # Sort descending by loss magnitude (largest losses first) + losses.sort(key=lambda x: x[0], reverse=True) + + accumulated = 0.0 + actions = [] + audit = AuditLog() + + for loss_amt, lot in losses: + if accumulated >= target_loss: + break + # sell entire lot for simplicity in this prototype + sell_qty = lot.quantity + expected_gain = _lot_loss(lot) + action = HarvestAction(lot_id=lot.id, sell_qty=sell_qty, date=date.today(), expected_gain=expected_gain) + actions.append(action) + accumulated += loss_amt + audit.entries.append({ + "lot_id": lot.id, + "realized_loss": loss_amt, + "basis": lot.basis, + "market_value": lot.market_value, + }) + + metadata = {"target_loss": target_loss, "realized_loss": accumulated} + plan = PlanDelta(actions=actions, metadata=metadata) + return plan, audit diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d4b7524 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["setuptools>=61.0","wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "idea115-taxalpha-studio-declarative" +version = "0.1.0" +description = "TaxAlpha Studio: Declarative tax-aware portfolio optimization (Phase 0 skeleton)" +readme = "README.md" +license = {text = "MIT"} +authors = [{name = "OpenCode Agent"}] +requires-python = ">=3.8" + +[project.urls] +Home = "https://example.com/idea115" diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..5755c6a --- /dev/null +++ b/test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Installing package in editable mode..." +python3 -m pip install -e . + +echo "Running pytest..." +pytest -q + +echo "Building package for verification..." +python3 -m build + +echo "All done." diff --git a/tests/test_solver.py b/tests/test_solver.py new file mode 100644 index 0000000..fc5b00a --- /dev/null +++ b/tests/test_solver.py @@ -0,0 +1,16 @@ +from idea115_taxalpha_studio_declarative.adapters.custodian_adapter import fetch_mock_custodian_lots +from idea115_taxalpha_studio_declarative.solver import optimize_harvest + + +def test_optimize_harvest_realizes_target_loss(): + lots = fetch_mock_custodian_lots() + # calculate total available loss from mock data: lotA loss = (80-100)*10 = -200 -> loss 200 + # lotC loss = (150-200)*2 = -100 -> loss 100 + # total loss available = 300 + plan, audit = optimize_harvest(lots, target_loss=250) + # Ensure we realized at least the requested loss + assert plan.metadata["realized_loss"] >= 250 + # Actions should include the largest-loss lots first (lotA then lotC) + ids = [a.lot_id for a in plan.actions] + assert "lotA" in ids + assert "lotC" in ids or plan.metadata["realized_loss"] >= 300