Agentic trading systems need a vocabulary for positions, instruments, and portfolios before they can reason about risk or execute trades. Finstruments is a Python library that provides exactly that: Pydantic-based models for forwards, options, trades, and portfolios. It sits one layer below orchestration frameworks, offering composable primitives that agents can call, extend, and serialize.
The library shipped with 72 points on Hacker News and 21 comments debating whether it overlaps with existing quant libraries or fills a gap. The answer depends on whether you need agent-friendly building blocks or a full backtesting suite. Finstruments is not QuantLib. It is a modeling layer designed for extensibility, not pricing engines.
What Finstruments Models
The library provides five core abstractions:
- Instrument: Base class for forwards, options, and custom derivatives. Each instrument carries metadata (ticker, expiry, strike) and valuation logic.
- Position: Links an instrument to a quantity and entry price. Positions track unrealized P&L and cost basis.
- Trade: Records a transaction (buy or sell) with timestamp, price, and fees. Trades update positions.
- Portfolio: Aggregates positions and trades. Exposes total value, cash balance, and position-level metrics.
- Asset: Represents the underlying (stock, commodity, crypto). Instruments reference assets.
All models use Pydantic v2, so validation happens at instantiation. Serialization to JSON is built in. This matters for agents because tool calls often pass structured data between LLM reasoning steps and execution environments.
Tool Boundary Patterns
When you integrate Finstruments into an agent system, you face a design choice: does each instrument become a separate tool, or do agents compose instruments programmatically?
Option 1: Instrument-per-tool
Each instrument type (forward, call option, put option) becomes a distinct tool with its own schema. The agent selects create_call_option or create_forward based on reasoning context. This approach keeps tool schemas small and legible to the LLM, but it fragments the instrument namespace. Adding a new derivative means registering a new tool.
Option 2: Programmatic composition
The agent calls a single create_instrument tool that accepts an instrument type parameter. The tool internally dispatches to the correct Finstruments class. This reduces tool count but shifts complexity into parameter validation. The LLM must understand the full instrument taxonomy to pass valid arguments.
Option 3: Portfolio-level tools
The agent never touches instruments directly. It calls add_position, execute_trade, or get_portfolio_summary. The tool layer wraps Finstruments models and exposes only portfolio operations. This hides instrument details but limits the agent’s ability to reason about individual derivatives.
Most production systems use a hybrid: portfolio-level tools for common operations, instrument-level tools for custom derivatives, and a fallback execute_code tool for edge cases.
State Persistence and Reconciliation
Finstruments models are stateless. A Portfolio object holds positions and trades in memory, but the library does not provide a database layer. When multiple agents or execution threads operate on the same portfolio, you need external state management.
Three common patterns:
-
Event sourcing: Store trades as an append-only log. Rebuild portfolio state by replaying trades. This gives you audit trails and time-travel debugging, but it requires a message bus (Kafka, Redis Streams) and replay logic.
-
Snapshot + delta: Persist portfolio snapshots at intervals (hourly, daily). Store trades since the last snapshot. On restart, load the snapshot and apply deltas. This reduces replay time but complicates schema migrations when you add new instrument types.
-
Database-backed models: Subclass Finstruments models to add SQLAlchemy or Pydantic-SQLModel mixins. Persist positions and trades to Postgres. This is the simplest approach for single-agent systems but introduces ORM overhead and transaction boundaries.
Reconciliation becomes critical when agents execute trades concurrently. If Agent A buys 100 shares and Agent B sells 50 shares of the same ticker, the portfolio must serialize updates or use optimistic locking. Finstruments does not enforce this. You need application-level coordination.
Extensibility and Schema Evolution
The library’s extensibility pattern is inheritance. To model a new instrument (barrier option, variance swap, credit default swap), you subclass Instrument and override valuation methods.
from finstruments import Instrument
from pydantic import Field
class BarrierOption(Instrument):
barrier_level: float = Field(..., description="Knock-in or knock-out price")
barrier_type: str = Field(..., pattern="^(up-and-in|up-and-out|down-and-in|down-and-out)$")
def value(self, spot_price: float) -> float:
# Custom valuation logic
if self.barrier_type == "up-and-out" and spot_price >= self.barrier_level:
return 0.0
return self._black_scholes_value(spot_price)
This works until you need to version instruments. If you change the BarrierOption schema (add a new field, rename barrier_level), old serialized positions break. Pydantic’s model_validate will reject JSON that does not match the current schema.
Three mitigation strategies:
| Strategy | Trade-off | When to use |
|---|---|---|
| Schema versioning | Add a schema_version field. Write migration functions for each version. | When you control all serialized data and can run migrations offline. |
| Alias fields | Use Pydantic’s Field(alias=...) to accept old field names. | When schema changes are additive (new optional fields). |
| Separate models | Create BarrierOptionV1, BarrierOptionV2. Agents specify version explicitly. | When breaking changes are frequent and you need parallel support. |
Most teams start with alias fields and migrate to schema versioning when they hit the third breaking change.
Observability Hooks
Finstruments models emit no telemetry by default. If you want to trace instrument creation, position updates, or portfolio rebalancing, you need to wrap method calls.
A minimal tracing layer:
from functools import wraps
import structlog
logger = structlog.get_logger()
def trace_instrument(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
logger.info("instrument_call", method=func.__name__, instrument=self.__class__.__name__)
result = func(self, *args, **kwargs)
logger.info("instrument_result", method=func.__name__, result=result)
return result
return wrapper
class TracedPortfolio(Portfolio):
@trace_instrument
def add_position(self, position):
return super().add_position(position)
This gives you structured logs for every portfolio operation. Pair it with OpenTelemetry spans if you need distributed tracing across agent calls, execution engines, and market data feeds.
Deployment Shape
Finstruments is a library, not a service. It runs in-process with your agent runtime. Typical deployment patterns:
- Embedded in agent: Import Finstruments directly into your LangChain, CrewAI, or AutoGen agent. Tools call library methods. State lives in memory or a local SQLite database.
- Behind a FastAPI service: Wrap Finstruments in a REST API. Agents call HTTP endpoints. State persists to Postgres. This adds latency but isolates the instrument layer from agent code.
- Lambda functions: Package Finstruments with AWS Lambda. Each tool call triggers a function. State lives in DynamoDB. This scales to zero but complicates transaction boundaries.
The embedded pattern is fastest but couples agent and instrument versions. The service pattern decouples them but adds network hops. The Lambda pattern scales best but makes reconciliation harder.
Failure Modes
Invalid instrument parameters: Pydantic validation catches most errors at instantiation (negative strike prices, expiry dates in the past). But it does not validate market-specific constraints (option strikes must align with exchange tick sizes). You need domain-specific validators.
Stale market data: Finstruments models accept spot prices as arguments. If your agent caches prices or uses delayed feeds, valuations drift. The library does not fetch live data. You need a separate market data layer.
Precision loss: Python floats have 15-17 decimal digits of precision. For high-frequency trading or large notional values, this causes rounding errors. Consider using decimal.Decimal for position quantities and prices.
Concurrency bugs: If two agents modify the same portfolio without locking, updates can interleave. Finstruments does not provide thread-safe collections. Use threading.Lock or a database with row-level locking.
Technical Verdict
Use Finstruments when:
- You are building an agent-driven trading system and need composable instrument models.
- You want Pydantic validation and JSON serialization out of the box.
- You plan to extend the library with custom derivatives or asset classes.
- You prefer a lightweight library over a full quant framework.
Avoid Finstruments when:
- You need production-grade pricing engines (use QuantLib or Bloomberg APIs).
- You require built-in state persistence or transaction management.
- Your system already uses a proprietary instrument model and migration cost is high.
- You need real-time market data integration (the library is data-agnostic).
Finstruments is plumbing, not a platform. It gives agents a vocabulary for financial instruments but leaves orchestration, execution, and risk management to other layers. If you are building agentic trading tools, this is the kind of foundational library you either adopt early or end up reimplementing.