When you hand an agent a financial modeling library, you expose a type system, a state hierarchy, and composition rules. If those boundaries are fuzzy, the agent will create portfolios that double-count collateral or price exotic options with missing parameters. Finstruments is a Python library built around position, trade, and portfolio models with out-of-the-box support for forwards and options. The library uses Pydantic for instrument definitions and provides base classes for extension, which creates natural boundaries for agent tool schemas.
The Position-Trade-Portfolio Hierarchy
Finstruments organizes financial objects into three layers:
- Instrument: A forward, option, or custom derivative with pricing logic.
- Position: A quantity of an instrument with entry price and current valuation.
- Trade: A timestamped transaction that creates or modifies a position.
- Portfolio: A collection of positions with aggregated risk metrics.
This hierarchy creates natural tool boundaries. An agent calling create_position cannot accidentally bypass trade logging. A portfolio valuation function cannot silently ignore a position’s collateral requirements. The state model enforces accounting rules at the type level.
The risk is that agents will try to shortcut the hierarchy. If an LLM sees a Position object and tries to instantiate it directly without a corresponding Trade, you lose audit history. The library needs to expose only the safe entry points as tools.
Instrument Extension and Tool Schema Mapping
Finstruments uses Pydantic models for instrument definitions. The library provides base classes like finstruments.Instrument, finstruments.Forward, and finstruments.Option. The extension pattern allows custom instruments through subclassing. When you expose instruments to an agent, the tool schema inherits Pydantic’s validation. The agent cannot pass a negative strike or a malformed date. But Pydantic does not know about domain constraints. A forward on a non-existent ticker will validate but fail at pricing time.
The extension pattern matters because agents need to create custom instruments. The library requires subclassing base instrument classes, which means the agent must generate Python code and execute it. This limits runtime flexibility but ensures type safety. For agent workloads, you typically pre-define instrument types and expose them as discrete tools rather than allowing arbitrary code generation.
Tool schemas used by LLMs to understand function signatures inherit Pydantic validators, so constraints appear in the function description the agent reads. This means domain validation must happen at the orchestration layer, not inside finstruments itself. The following examples show recommended orchestration wrappers that add domain-specific validation on top of finstruments base classes. These wrappers are necessary because finstruments provides type safety but not business logic validation.
# Orchestration wrapper pattern: Domain validation for finstruments.Forward
# Wraps finstruments.Forward with additional agent-safe validation
from pydantic import BaseModel, validator
from datetime import date, datetime
class ForwardContract(BaseModel):
"""
Wraps finstruments.Forward with domain validation.
Prevents agents from creating invalid forwards.
"""
underlying: str
strike: float
expiry: date
quantity: int
@validator('strike')
def strike_must_be_positive(cls, v):
if v <= 0:
raise ValueError("Strike must be positive")
return v
@validator('expiry')
def expiry_must_be_future(cls, v):
if v < datetime.now().date():
raise ValueError("Cannot create expired forward")
return v
def to_finstruments_forward(self):
"""Convert to actual finstruments.Forward object."""
from finstruments import Forward
return Forward(
underlying=self.underlying,
strike=self.strike,
expiry=self.expiry,
quantity=self.quantity
)
The key is that validation happens at instantiation time, before the agent can use the instrument in calculations. The wrapper exposes a clean interface to the LLM while delegating actual pricing and accounting to finstruments.
State Management and Double-Counting Risks
A portfolio is stateful. Adding a position changes net exposure. Removing a position affects margin requirements. If an agent makes two tool calls in parallel (one to add a long position, one to add a short position in the same underlying), the portfolio needs to serialize those updates or risk inconsistent state.
The orchestration layer must handle concurrency control. Options include:
| Pattern | Trade-off | When It Breaks |
|---|---|---|
| Single-threaded executor | Simple, no race conditions | Slow for large portfolios; agent waits for each operation to complete |
| Optimistic locking | Fast, scales well | Retry storms if contention is high; agent sees stale portfolio state |
| Event sourcing | Full audit trail, replayable | Complex, requires event store; adds latency to every tool call |
| Immutable portfolios | No locking needed | Memory overhead; agent creates hundreds of portfolio copies during exploration |
For agent workloads, immutable portfolios with copy-on-write are often the right choice. Each tool call returns a new portfolio object. The orchestrator decides which version to commit. This prevents silent overwrites but requires careful memory management if the agent generates hundreds of hypothetical portfolios.
Validation Layers for Exotic Options
An exotic option might have path-dependent payoffs, barrier levels, or knock-in conditions. If an agent calls a pricing function with missing or contradictory parameters, the error should surface immediately, not after the result propagates into a risk report.
The orchestration layer should expose domain validation as part of the tool schema. The following pattern shows how to wrap finstruments option objects with domain-specific validation logic that catches invalid barrier configurations before they reach the pricing engine.
# Orchestration wrapper pattern: Domain validation for finstruments barrier options
# Wraps finstruments.Option with barrier-specific constraints
from pydantic import BaseModel, validator
class BarrierOption(BaseModel):
"""
Wraps finstruments.Option with barrier validation.
Prevents agents from creating invalid barrier structures.
"""
strike: float
barrier: float
option_type: str # "down-and-out-put", "up-and-out-call", etc.
@validator('barrier')
def validate_barrier_level(cls, v, values):
option_type = values.get('option_type')
strike = values.get('strike')
if option_type == 'down-and-out-put' and v >= strike:
raise ValueError(
"Barrier must be below strike for down-and-out put"
)
if option_type == 'up-and-out-call' and v <= strike:
raise ValueError(
"Barrier must be above strike for up-and-out call"
)
return v
def to_finstruments_option(self):
"""Convert to actual finstruments.Option with barrier metadata."""
from finstruments import Option
return Option(
strike=self.strike,
option_type=self.option_type,
metadata={'barrier': self.barrier}
)
This moves validation into the model definition. The agent sees a clear error message if it tries to create an invalid instrument. But validators add latency. For high-frequency tool calls, you might want to batch validation or defer it to a separate step.
Observability for Agent-Driven Portfolios
When an agent builds a portfolio, the system must trace:
- Which tool calls created which positions.
- What inputs were used for pricing.
- Whether any positions were silently dropped due to validation errors.
The orchestration layer must add observability hooks. A simple pattern is to wrap every tool call in a logging decorator that captures inputs, outputs, and errors. For production systems, emit structured events to a trace store (OpenTelemetry, Honeycomb, etc.).
Correlating agent reasoning with portfolio state requires storing the agent’s chain-of-thought alongside the portfolio mutations. If the agent says “I am hedging delta exposure” but the resulting portfolio has no hedge, you need to replay the tool calls and identify where the logic diverged. This means capturing not only the final portfolio state but also intermediate states and the reasoning that led to each mutation.
Deployment Architecture and Failure Modes
Finstruments is a library, not a service. Serverless deployments require bundling the Python runtime and all dependencies, adding cold-start latency. For long-running agents, it means managing library versions and ensuring the agent does not import a stale pricing model.
Common failure modes:
- Stale market data: The agent prices an option using yesterday’s spot price because the data feed did not refresh.
- Missing instrument definitions: The agent tries to create a custom derivative but the extension code was not loaded into the runtime.
- Silent calculation errors: A pricing function returns
NaNdue to a divide-by-zero, and the agent interprets it as a valid price.
Mitigation strategies:
- Pin library versions and use a virtual environment per agent instance.
- Add health checks that verify market data freshness before allowing tool calls.
- Wrap all pricing functions in error handlers that return structured errors, not raw exceptions.
Technical Verdict
Use finstruments when you need composable financial primitives that agents can instantiate and price without hardcoded logic. The Pydantic-based extension model maps cleanly to tool schemas, and the position-trade-portfolio hierarchy enforces accounting boundaries.
For production use, you must add:
- Orchestration wrappers that validate domain constraints (barrier levels, expiry dates, ticker existence) before passing data to finstruments.
- Concurrency control through immutable portfolios, optimistic locking, or event sourcing to prevent race conditions in multi-agent systems.
- Observability hooks that trace tool calls, capture intermediate portfolio states, and correlate agent reasoning with mutations.
- Error handling layers that catch silent calculation failures (NaN, divide-by-zero) and return structured errors to the agent.
The ideal use case is prototyping agent-driven portfolio strategies where you want to iterate quickly on instrument definitions without rewriting pricing logic. Finstruments provides the calculation layer. You build orchestration, validation, and observability around it.