build(agent): melter#14fd4b iteration

This commit is contained in:
agent-14fd4b738639d573 2026-04-24 21:05:42 +02:00
parent df54551707
commit 570c27c6ba
12 changed files with 335 additions and 1 deletions

21
.gitignore vendored Normal file
View File

@ -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

32
AGENTS.md Normal file
View File

@ -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`

View File

@ -1,3 +1,17 @@
# idea115-taxalpha-studio-declarative
Source logic for Idea #115
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

View File

@ -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",
]

View File

@ -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

View File

@ -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),
]

View File

@ -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}

View File

@ -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)

View File

@ -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

15
pyproject.toml Normal file
View File

@ -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"

13
test.sh Executable file
View File

@ -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."

16
tests/test_solver.py Normal file
View File

@ -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