build(agent): melter#14fd4b iteration
This commit is contained in:
parent
df54551707
commit
570c27c6ba
|
|
@ -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
|
||||
|
|
@ -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`
|
||||
16
README.md
16
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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),
|
||||
]
|
||||
|
|
@ -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}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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."
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue