This project is an agentic trading and analysis system built on:
- FastAPI for HTTP APIs
- LangGraph and LangChain for agent workflows
- OpenAI models for reasoning
- A simple plugin engine so new agents and providers can be “plugged in”
- Package name:
finagent - Version:
0.0.1(defined in pyproject.toml)
Once published to PyPI, you’ll be able to install it with:
pip install finagentThis document explains how other developers can create and plug in their own agents.
At a high level:
src/agents/multimodel_trading/agent.pyhosts the main FastAPI app, LangGraph workflow, and CLI for the trading agent.src/agents/deepquant/agent.pywraps the DeepQuant backtesting engine as an agent (see DeepQuant Agent).src/core/plugin_base.pydefines the core plugin interfaces and registry.src/agents/news_yfinance/agent.pyis a concrete plugin that provides market news via yfinance.src/agents/weather_demo/agent.pyshows a LangChain tool-style agent example.src/api_service.pyis the central FastAPI gateway that auto-discovers agent HTTP APIs.src/agent_eval.pyis a generic CLI harness for invoking agent-specific Typer commands.
The plugin system is intentionally small and composable, so you can:
- Add new providers (e.g. different news sources, data sources, execution backends).
- Switch providers without changing the main engine.
- Extend to new capability types over time.
All plugin-related types live in plugin_base.py.
Key pieces:
AgentPlugin: abstract base for future multi-capability agents.MarketNewsProvider: a Protocol for “news agents”.PluginRegistry: singleton registry that tracks providers and exposes lookup methods.
The core definitions:
class AgentPlugin(ABC):
name: str
description: str
capabilities: List[str]
@abstractmethod
def invoke(self, capability: str, payload: Dict[str, Any]) -> Any:
raise NotImplementedError
class MarketNewsProvider(Protocol):
name: str
def get_news(self, asset_symbol: str, limit: int = 5) -> List[str]:
...
class PluginRegistry:
def __init__(self) -> None:
self._news_providers: Dict[str, MarketNewsProvider] = {}
def register_news_provider(self, provider: MarketNewsProvider) -> None:
self._news_providers[provider.name] = provider
def get_news_provider(self, name: Optional[str] = None) -> Optional[MarketNewsProvider]:
if name is None:
if not self._news_providers:
return None
return next(iter(self._news_providers.values()))
return self._news_providers.get(name)
plugin_registry = PluginRegistry()You should think of:
MarketNewsProvideras “an agent that knows how to get news”.PluginRegistryas “the directory of available agents for each capability”.
The file agent.py provides a concrete example of a plugin implementing MarketNewsProvider.
from typing import List
import yfinance as yf
from .plugin_base import MarketNewsProvider, plugin_registry
class YFinanceNewsAgent:
name = "yfinance_news"
def get_news(self, asset_symbol: str, limit: int = 5) -> List[str]:
ticker = yf.Ticker(asset_symbol)
news = getattr(ticker, "news", None)
if not news and hasattr(ticker, "get_news"):
try:
news = ticker.get_news()
except Exception:
news = None
if not news:
return []
formatted: List[str] = []
for item in news[:limit]:
title = item.get("title")
summary = item.get("summary") or item.get("content") or ""
publisher = item.get("publisher") or item.get("source") or ""
if not title:
continue
formatted.append(
f"Title: {title} | Publisher: {publisher} | Summary: {summary}".strip()
)
return formatted
plugin_registry.register_news_provider(YFinanceNewsAgent())Important points:
- The agent has a unique
name. - It implements
get_news(asset_symbol, limit) -> List[str]. - It calls
plugin_registry.register_news_provider(...)at import time so the engine can discover it.
The main engine in agent.py uses FinancialDataFetcher as the abstraction boundary for external data.
Relevant code (simplified):
from core.plugin_base import plugin_registry
class FinancialDataFetcher:
def __init__(self, news_provider_name: Optional[str] = None):
self.news_provider_name = news_provider_name
def fetch_price_data(self, asset_symbol: str, start: str, end: str):
...
def fetch_news(self, asset_symbol: str) -> List[str]:
provider = plugin_registry.get_news_provider(self.news_provider_name)
if provider is None:
return []
try:
return provider.get_news(asset_symbol, limit=5)
except Exception:
return []Integration points:
build_finagent_graph()instantiatesFinancialDataFetcherand passes it into the application graph.- The FastAPI
/fetch-financial-dataendpoint and the CLIfetch-financial-datacommand both callfetch_news, which is now fully plugin-based.
This means:
- To change where news comes from, you do not edit
multimodel_trading.py. - You add or modify a plugin and optionally select it by name when creating
FinancialDataFetcher.
This is the flow other developers should follow to add a new “news agent” plugin.
-
Create a new module under
src/agentsFor example:
- Path:
src/agents/news_myprovider_agent.py
- Path:
-
Implement the
MarketNewsProviderinterfaceThe minimal shape:
from typing import List from .plugin_base import MarketNewsProvider, plugin_registry class MyProviderNewsAgent: name = "myprovider_news" def get_news(self, asset_symbol: str, limit: int = 5) -> List[str]: # 1) Call your external API/service here items = [] # replace with real fetch # 2) Normalize to a list of human-readable strings results: List[str] = [] for item in items[:limit]: title = item["title"] summary = item.get("summary", "") publisher = item.get("publisher", "") results.append( f"Title: {title} | Publisher: {publisher} | Summary: {summary}".strip() ) return results
-
Register the agent with the plugin registry
At the bottom of the same file:
plugin_registry.register_news_provider(MyProviderNewsAgent())
This ensures that importing
news_myprovider_agentmakes the agent available to the engine. -
Ensure the module is imported somewhere
Typical options:
-
Import it explicitly in
multimodel_trading.py. -
Or add a small import hub in
src/agents/__init__.pythat imports all agent modules.Once the module is imported, your agent is registered and ready.
-
(Optional) Select your agent by name
By default,
plugin_registry.get_news_provider()returns the first registered provider. If you want to force a specific agent:from agents.plugin_base import plugin_registry from agents import news_myprovider_agent # noqa: F401 (ensure import) fetcher = FinancialDataFetcher(news_provider_name="myprovider_news")
You can wire this into:
build_finagent_graph()- a dedicated CLI command
- a configuration mechanism (env var, config file, etc.)
The current MarketNewsProvider is just one capability. You can extend the same pattern for other types of agents:
MarketDataProvider– agents that fetch price, order book, on-chain data.ExecutionAgent– agents that talk to paper trading or brokerage APIs.ResearchAgent– long-form analysis agents that call LLMs and tools.
The pattern is:
-
Define a Protocol in
plugin_base.py, for example:class MarketDataProvider(Protocol): name: str def get_prices(self, asset_symbol: str, start: str, end: str) -> list[dict]: ...
-
Add a corresponding registry section in
PluginRegistry:self._data_providers: Dict[str, MarketDataProvider] = {} def register_data_provider(self, provider: MarketDataProvider) -> None: self._data_providers[provider.name] = provider def get_data_provider(self, name: Optional[str] = None) -> Optional[MarketDataProvider]: ...
-
Implement concrete providers in
src/agents/.... -
Register them on import, exactly like the news agents.
-
Use the registry from the engine code instead of hard-coding integrations.
The file agent.py shows another flavor of “agent”:
- Tools are defined with
@tool. - A chat model is initialized.
create_agentbuilds an agent that uses those tools.
This pattern is ideal when:
- You want an LLM-driven agent that decides when to call tools.
- You want a structured response format (e.g.,
ResponseFormatdataclass).
You can combine this with the plugin registry by:
- Implementing tools that internally call plugin-based providers.
- Or wrapping plugin-based providers behind LangChain tools, so the same agent can switch providers without changes to the tool interface.
- The plugin engine is centered on simple Protocols plus a shared
PluginRegistry. - Concrete agents live in
src/agents, implement a capability-specific Protocol, and register themselves. - The main engine (FastAPI, LangGraph, CLI) depends only on the registry and capability interfaces, not on concrete implementations.
- Adding a new agent is mainly:
- Creating a module under
src/agents. - Implementing the right Protocol.
- Registering with the
PluginRegistry. - Ensuring the module is imported.
With this pattern, you can grow FinAgent into a richer agentic system where new capabilities can be plugged in with minimal friction.
HTTP-capable agents expose an APIRouter and are discovered at startup by the central API service in api_service.py.
Agent pattern:
- Each agent lives under
src/agents/<agent_name>/. - The main module is
src/agents/<agent_name>/agent.py. - It defines:
- A FastAPI
appif it wants to run standalone. - An
APIRouternamedrouterwith its HTTP endpoints.
- A FastAPI
Example (multimodel trading) in agent.py:
appis created withFastAPI(...).- A router is created and endpoints are attached:
router.get("/fetch-financial-data")(...)router.post("/trade/decision")(...)
- The router is included into the app:
app.include_router(router)
The central gateway in api_service.py will:
- Scan
src/agentsfor packages (folders with__init__.py). - Import
agents.<agent_name>.agent. - Look for a top-level
router: APIRouter. - Mount it under
/api/agents/<agent-slug>/...where<agent-slug>is the folder name with_replaced by-.
For example:
- Agent package:
multimodel_trading - HTTP routes become:
GET /api/agents/multimodel-trading/fetch-financial-dataPOST /api/agents/multimodel-trading/trade/decision
To run the central API service:
make backendor explicitly:
PYTHONPATH=src uv run uvicorn api_service:app --host 0.0.0.0 --port 8000You can list discovered agents via:
curl http://127.0.0.1:8000/api/agentsand open the browsable docs at:
http://127.0.0.1:8000/docs
Agents can also expose a Typer-based CLI so you can test or script them uniformly via agent_eval.py.
CLI pattern:
- In
src/agents/<agent_name>/agent.py:- Define a top-level
clivariable of typetyper.Typer. - Add commands on
clifor agent-specific operations.
- Define a top-level
Example (multimodel trading) in agent.py:
cli = typer.Typer()- Commands:
run-api– start the FastAPI backend.test-retrieval– exercise the vector retrieval module.fetch-financial-data– CLI wrapper around the data fetcher.
The evaluator in agent_eval.py works as follows:
- It takes
--agent(or-a) specifying the agent package name. - It imports
agents.<agent_name>.agent. - It looks up
cliand forwards any extra arguments after--directly to that Typer app.
Usage pattern:
From the project root, with PYTHONPATH=src:
PYTHONPATH=src uv run src/agent_eval.py --helpRun a multimodel trading CLI command via the evaluator:
PYTHONPATH=src uv run src/agent_eval.py exec --agent multimodel_trading -- --help
PYTHONPATH=src uv run src/agent_eval.py exec --agent multimodel_trading -- test-retrieval
PYTHONPATH=src uv run src/agent_eval.py exec --agent multimodel_trading -- fetch-financial-data --asset-symbol AAPL --start-date 2024-01-01 --end-date 2024-01-10Run the weather agent via the evaluator:
PYTHONPATH=src uv run src/agent_eval.py exec --agent weather -- --city SydneyNotes:
--agentuses the human-friendly agent name (slug) where dashes are allowed. Dashes are automatically converted to underscores to resolve the Python package, so--agent weathermaps toagents.weather.agent,--agent agent-gamemaps toagents.agent_game.agent, and so on.- Everything after
--is passed verbatim to the agent’scli.
To add a new CLI-capable agent:
- Create
src/agents/<agent_name>/agent.pyand ensure there is an__init__.pyin the folder. - Define a
cli = typer.Typer()instance. - Add your commands on
cli(for example,@cli.command()functions). - Optionally add a
if __name__ == "__main__": cli()block if you want to run the agent module directly. - Use
agent_eval.pyto invoke the commands uniformly, without caring about the underlying module path.