finlet 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
finlet/__init__.py ADDED
File without changes
finlet/cli.py ADDED
@@ -0,0 +1,213 @@
1
+ """CLI entrypoint — finlet serve / session / plugins / mcp."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+
8
+ import typer
9
+
10
+ from finlet.config import PLUGINS_CONFIG_PATH
11
+
12
+ _API_URL = os.environ.get("FINLET_API_URL", "https://finlet.dev")
13
+
14
+
15
+ def _api_headers(api_key: str | None) -> dict[str, str]:
16
+ """Build request headers, including Bearer auth if an API key is provided."""
17
+ headers: dict[str, str] = {}
18
+ if api_key:
19
+ headers["Authorization"] = f"Bearer {api_key}"
20
+ return headers
21
+
22
+
23
+ app = typer.Typer(
24
+ name="finlet",
25
+ help="Finlet — Integrated Testing Environment for AI Trading Agent Reasoning",
26
+ no_args_is_help=True,
27
+ )
28
+
29
+ session_app = typer.Typer(help="Session management")
30
+ plugins_app = typer.Typer(help="Plugin management")
31
+ app.add_typer(session_app, name="session")
32
+ app.add_typer(plugins_app, name="plugins")
33
+
34
+
35
+ # ── serve ────────────────────────────────────────────────────
36
+
37
+
38
+ @app.command()
39
+ def serve(
40
+ host: str = typer.Option("0.0.0.0", envvar="FINLET_HOST", help="Bind address"),
41
+ port: int = typer.Option(8000, envvar="FINLET_PORT", help="Port number"),
42
+ reload: bool = typer.Option(False, help="Enable auto-reload for development"),
43
+ ) -> None:
44
+ """Start the API server and serve the dashboard."""
45
+ try:
46
+ import uvicorn
47
+ except ImportError:
48
+ typer.echo("Error: Server mode requires the full Finlet installation.", err=True)
49
+ typer.echo("Install with: pip install finlet[server]", err=True)
50
+ raise typer.Exit(1)
51
+
52
+ uvicorn.run(
53
+ "finlet.api.app:create_app",
54
+ factory=True,
55
+ host=host,
56
+ port=port,
57
+ reload=reload,
58
+ )
59
+
60
+
61
+ # ── session ──────────────────────────────────────────────────
62
+
63
+
64
+ @session_app.command("create")
65
+ def session_create(
66
+ name: str = typer.Option(..., "--name", "-n", help="Session name"),
67
+ start: str = typer.Option(..., "--start", "-s", help="Start datetime (ISO format, e.g. 2024-01-01)"),
68
+ capital: float = typer.Option(100_000.0, "--capital", "-c", help="Initial capital"),
69
+ universe: str = typer.Option("", "--universe", "-u", help="Comma-separated ticker symbols"),
70
+ api_key: str | None = typer.Option(
71
+ None, "--api-key", "-k", envvar="FINLET_API_KEY", help="API key for authentication"
72
+ ),
73
+ ) -> None:
74
+ """Create a new simulation session."""
75
+ import httpx
76
+
77
+ tickers = [t.strip() for t in universe.split(",") if t.strip()] if universe else []
78
+ body = {
79
+ "name": name,
80
+ "start_time": start,
81
+ "initial_capital": capital,
82
+ "universe": tickers,
83
+ }
84
+
85
+ try:
86
+ resp = httpx.post(f"{_API_URL}/sessions", json=body, headers=_api_headers(api_key), timeout=10)
87
+ resp.raise_for_status()
88
+ data = resp.json()
89
+ typer.echo(f"Session created: {data['id']} ({data['name']})")
90
+ typer.echo(f" Start: {data['config']['start_time']}")
91
+ typer.echo(f" Capital: ${data['config']['initial_capital']:,.2f}")
92
+ if tickers:
93
+ typer.echo(f" Universe: {', '.join(tickers)}")
94
+ except httpx.ConnectError:
95
+ typer.echo("Error: Cannot connect to Finlet server. Run 'finlet serve' first.", err=True)
96
+ raise typer.Exit(1)
97
+ except httpx.HTTPStatusError as e:
98
+ typer.echo(f"Error: {e.response.json().get('detail', str(e))}", err=True)
99
+ raise typer.Exit(1)
100
+
101
+
102
+ @session_app.command("list")
103
+ def session_list(
104
+ api_key: str | None = typer.Option(
105
+ None, "--api-key", "-k", envvar="FINLET_API_KEY", help="API key for authentication"
106
+ ),
107
+ ) -> None:
108
+ """List all sessions."""
109
+ import httpx
110
+
111
+ try:
112
+ resp = httpx.get(f"{_API_URL}/sessions", headers=_api_headers(api_key), timeout=10)
113
+ resp.raise_for_status()
114
+ sessions = resp.json()
115
+ if not sessions:
116
+ typer.echo("No active sessions.")
117
+ return
118
+ for s in sessions:
119
+ clock_time = s.get("clock", {}).get("current_time", "unknown")
120
+ typer.echo(f" {s['id']} {s.get('name', s['id'])} [{s['status']}] {clock_time}")
121
+ except httpx.ConnectError:
122
+ typer.echo("Error: Cannot connect to Finlet server. Run 'finlet serve' first.", err=True)
123
+ raise typer.Exit(1)
124
+
125
+
126
+ @session_app.command("delete")
127
+ def session_delete(
128
+ session_id: str = typer.Argument(..., help="Session ID to delete"),
129
+ api_key: str | None = typer.Option(
130
+ None, "--api-key", "-k", envvar="FINLET_API_KEY", help="API key for authentication"
131
+ ),
132
+ ) -> None:
133
+ """Delete a session."""
134
+ import httpx
135
+
136
+ try:
137
+ resp = httpx.delete(f"{_API_URL}/sessions/{session_id}", headers=_api_headers(api_key), timeout=10)
138
+ if resp.status_code == 404:
139
+ typer.echo(f"Error: Session '{session_id}' not found.", err=True)
140
+ raise typer.Exit(1)
141
+ resp.raise_for_status()
142
+ typer.echo(f"Session deleted: {session_id}")
143
+ except httpx.ConnectError:
144
+ typer.echo("Error: Cannot connect to Finlet server. Run 'finlet serve' first.", err=True)
145
+ raise typer.Exit(1)
146
+ except httpx.HTTPStatusError as e:
147
+ typer.echo(f"Error: {e.response.json().get('detail', str(e))}", err=True)
148
+ raise typer.Exit(1)
149
+
150
+
151
+ # ── plugins ──────────────────────────────────────────────────
152
+
153
+
154
+ def _load_plugins_config() -> dict: # type: ignore[type-arg]
155
+ if PLUGINS_CONFIG_PATH.exists():
156
+ return json.loads(PLUGINS_CONFIG_PATH.read_text()) # type: ignore[no-any-return]
157
+ return {}
158
+
159
+
160
+ def _save_plugins_config(config: dict) -> None:
161
+ PLUGINS_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
162
+ PLUGINS_CONFIG_PATH.write_text(json.dumps(config, indent=2) + "\n")
163
+
164
+
165
+ @plugins_app.command("list")
166
+ def plugins_list() -> None:
167
+ """Show installed plugins and status."""
168
+ config = _load_plugins_config()
169
+ builtin = {
170
+ "price": {"type": "price", "requires_key": False},
171
+ "edgar": {"type": "filings", "requires_key": False},
172
+ "fred": {"type": "economic", "requires_key": True},
173
+ "finnhub": {"type": "news+fundamentals", "requires_key": True},
174
+ }
175
+ typer.echo("Plugins:")
176
+ for name, info in builtin.items():
177
+ status = "configured" if name in config else ("ready" if not info["requires_key"] else "needs API key")
178
+ typer.echo(f" {name:12s} [{info['type']:20s}] {status}")
179
+
180
+
181
+ @plugins_app.command("add")
182
+ def plugins_add(
183
+ name: str = typer.Argument(..., help="Plugin name (e.g. 'finnhub', 'fred')"),
184
+ api_key: str | None = typer.Option(None, "--api-key", "-k", help="API key"),
185
+ email: str | None = typer.Option(None, "--email", "-e", help="Email (for EDGAR User-Agent)"),
186
+ ) -> None:
187
+ """Configure a plugin with API key or settings."""
188
+ config = _load_plugins_config()
189
+ entry: dict[str, str] = {"enabled": "true"}
190
+ if api_key:
191
+ entry["api_key"] = api_key
192
+ if email:
193
+ entry["email"] = email
194
+ config[name] = entry
195
+ _save_plugins_config(config)
196
+ typer.echo(f"Plugin '{name}' configured. Restart server to apply.")
197
+
198
+
199
+ # ── mcp ──────────────────────────────────────────────────────
200
+
201
+
202
+ @app.command()
203
+ def mcp() -> None:
204
+ """Start MCP server over stdio (for Claude Code integration)."""
205
+ try:
206
+ from finlet.mcp.server import mcp as mcp_server
207
+ except ImportError:
208
+ typer.echo("Error: Local MCP server requires the full Finlet installation.", err=True)
209
+ typer.echo("Use MCP-over-HTTP with finlet.dev instead.", err=True)
210
+ typer.echo("Install server with: pip install finlet[server]", err=True)
211
+ raise typer.Exit(1)
212
+
213
+ mcp_server.run(transport="stdio")
finlet/config.py ADDED
@@ -0,0 +1,187 @@
1
+ """Centralized configuration for Finlet.
2
+
3
+ All environment variable access is consolidated here. Modules import
4
+ from ``finlet.config`` instead of calling ``os.environ`` / ``os.getenv``
5
+ directly.
6
+
7
+ Variables that need SSM resolution (secrets) still go through ``finlet.ssm``
8
+ and are not duplicated here — this module handles non-secret configuration
9
+ only.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+
18
+ # ── Filesystem paths (single source of truth) ────────────────
19
+ FINLET_DIR = Path.home() / ".finlet"
20
+ PLUGINS_CONFIG_PATH = FINLET_DIR / "plugins.json"
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class LoggingConfig:
25
+ """Logging settings."""
26
+
27
+ format: str = os.environ.get("FINLET_LOG_FORMAT", "json").lower()
28
+ level: str = os.environ.get("FINLET_LOG_LEVEL", "INFO").upper()
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class CORSConfig:
33
+ """CORS settings."""
34
+
35
+ origins_raw: str = os.environ.get("FINLET_CORS_ORIGINS", "").strip()
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class SecurityConfig:
40
+ """Security middleware settings."""
41
+
42
+ max_request_body_bytes: int = int(os.environ.get("FINLET_MAX_REQUEST_BODY_BYTES", str(1024 * 1024)))
43
+ hsts_enabled: bool = os.environ.get("FINLET_HSTS_ENABLED", "").lower() in (
44
+ "true",
45
+ "1",
46
+ "yes",
47
+ )
48
+ key_rotation_grace_hours: int = int(os.environ.get("FINLET_KEY_ROTATION_GRACE_HOURS", "24"))
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class RateLimitConfig:
53
+ """Rate limiting settings."""
54
+
55
+ reset_max_per_hour: int = int(os.environ.get("FINLET_RESET_RATE_LIMIT", "5"))
56
+ market_data_max_per_hour: int = int(os.environ.get("FINLET_MARKET_DATA_RATE_LIMIT", "100"))
57
+ trade_max_per_hour: int = int(os.environ.get("FINLET_TRADE_RATE_LIMIT", "50"))
58
+ plugin_rate_per_user_per_minute: int = int(os.environ.get("FINLET_PLUGIN_RATE_PER_USER", "30"))
59
+ plugin_rate_global_per_minute: int = int(os.environ.get("FINLET_PLUGIN_RATE_GLOBAL", "150"))
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class BillingConfig:
64
+ """Billing redirect settings."""
65
+
66
+ _allowed_raw: str = os.environ.get("FINLET_ALLOWED_REDIRECT_DOMAINS", "").strip()
67
+ _default_domains: tuple[str, ...] = ("finlet.dev", "localhost")
68
+
69
+ @property
70
+ def allowed_redirect_domains(self) -> list[str]:
71
+ parsed = [d.strip() for d in self._allowed_raw.split(",") if d.strip()]
72
+ return parsed or list(self._default_domains)
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class AdminConfig:
77
+ """Admin user settings."""
78
+
79
+ admin_user_ids_raw: str = os.environ.get("FINLET_ADMIN_USER_IDS", "").strip()
80
+
81
+ @property
82
+ def admin_user_ids(self) -> set[str]:
83
+ if not self.admin_user_ids_raw:
84
+ return set()
85
+ return {uid.strip() for uid in self.admin_user_ids_raw.split(",") if uid.strip()}
86
+
87
+
88
+ @dataclass(frozen=True, slots=True)
89
+ class MarketplaceConfig:
90
+ """Marketplace feature flag."""
91
+
92
+ enabled: bool = os.environ.get("FINLET_MARKETPLACE_ENABLED", "").lower() in (
93
+ "true",
94
+ "1",
95
+ "yes",
96
+ )
97
+
98
+
99
+ @dataclass(frozen=True, slots=True)
100
+ class SSMConfig:
101
+ """AWS SSM Parameter Store settings."""
102
+
103
+ enabled: bool = os.environ.get("FINLET_SSM_ENABLED", "1") != "0"
104
+ prefix: str = os.environ.get("FINLET_SSM_PREFIX", "/finlet/prod")
105
+ region: str = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class PricePluginConfig:
110
+ """Price plugin settings."""
111
+
112
+ bucket: str = os.environ.get("FINLET_PRICE_BUCKET", "finlet-data")
113
+
114
+
115
+ @dataclass(frozen=True, slots=True)
116
+ class FundamentalsPluginConfig:
117
+ """Fundamentals plugin settings."""
118
+
119
+ bucket: str = os.environ.get("FINLET_PRICE_BUCKET", "finlet-data")
120
+ prefix: str = os.environ.get("FINLET_FUNDAMENTALS_PREFIX", "fundamentals/quarterly")
121
+
122
+
123
+ @dataclass(frozen=True, slots=True)
124
+ class NewsPluginConfig:
125
+ """News plugin settings."""
126
+
127
+ bucket: str = os.environ.get("FINLET_PRICE_BUCKET", "finlet-data")
128
+ prefix: str = os.environ.get("FINLET_NEWS_PREFIX", "news/daily")
129
+
130
+
131
+ @dataclass(frozen=True, slots=True)
132
+ class SentimentPluginConfig:
133
+ """Sentiment plugin settings.
134
+
135
+ S3 path layout: sentiment/analyst_ratings/{TICKER}.parquet
136
+
137
+ Schema per row:
138
+ ticker: str — ticker symbol
139
+ rated_at: datetime UTC — rating issue date (ceiling-filtered)
140
+ firm: str — analyst firm name
141
+ action: str — Upgrade | Downgrade | Reiterated | Initiated | Maintains | Resumed
142
+ from_grade: str | None — prior grade
143
+ to_grade: str | None — new grade
144
+ price_target_from: float | None — prior price target
145
+ price_target_to: float | None — new price target
146
+ source: str — scraper source (e.g. "stockanalysis")
147
+ ingested_at: datetime UTC — ingestion timestamp
148
+ """
149
+
150
+ bucket: str = os.environ.get("FINLET_PRICE_BUCKET", "finlet-data")
151
+ prefix: str = os.environ.get("FINLET_SENTIMENT_PREFIX", "sentiment/analyst_ratings")
152
+
153
+
154
+ @dataclass(frozen=True, slots=True)
155
+ class SessionAuthConfig:
156
+ """Session authentication settings."""
157
+
158
+ expiry_hours: int = 24
159
+ token_bytes: int = 32
160
+ csrf_token_bytes: int = 32
161
+ cookie_name: str = "finlet_session"
162
+
163
+
164
+ @dataclass(frozen=True, slots=True)
165
+ class Settings:
166
+ """Top-level settings container.
167
+
168
+ Instantiate once at import time. All sub-configs read from environment
169
+ variables at construction time.
170
+ """
171
+
172
+ logging: LoggingConfig = field(default_factory=LoggingConfig)
173
+ cors: CORSConfig = field(default_factory=CORSConfig)
174
+ security: SecurityConfig = field(default_factory=SecurityConfig)
175
+ rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)
176
+ billing: BillingConfig = field(default_factory=BillingConfig)
177
+ admin: AdminConfig = field(default_factory=AdminConfig)
178
+ marketplace: MarketplaceConfig = field(default_factory=MarketplaceConfig)
179
+ ssm: SSMConfig = field(default_factory=SSMConfig)
180
+ price_plugin: PricePluginConfig = field(default_factory=PricePluginConfig)
181
+ fundamentals_plugin: FundamentalsPluginConfig = field(default_factory=FundamentalsPluginConfig)
182
+ news_plugin: NewsPluginConfig = field(default_factory=NewsPluginConfig)
183
+ sentiment_plugin: SentimentPluginConfig = field(default_factory=SentimentPluginConfig)
184
+ session_auth: SessionAuthConfig = field(default_factory=SessionAuthConfig)
185
+
186
+
187
+ settings = Settings()
@@ -0,0 +1,453 @@
1
+ Metadata-Version: 2.4
2
+ Name: finlet
3
+ Version: 0.0.1
4
+ Summary: A historical market simulation environment for testing AI trading agent reasoning against real market data.
5
+ Author: Finlet Contributors
6
+ License-Expression: LicenseRef-Proprietary
7
+ License-File: LICENSE
8
+ Keywords: agent,ai,backtesting,llm,mcp,simulation,trading
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: FastAPI
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: Other/Proprietary License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Office/Business :: Financial :: Investment
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: httpx>=0.27
18
+ Requires-Dist: rich>=13.0
19
+ Requires-Dist: typer>=0.12
20
+ Provides-Extra: dev
21
+ Requires-Dist: bandit>=1.7; extra == 'dev'
22
+ Requires-Dist: httpx>=0.27; extra == 'dev'
23
+ Requires-Dist: hypothesis>=6.100; extra == 'dev'
24
+ Requires-Dist: mypy-boto3-s3>=1.34; extra == 'dev'
25
+ Requires-Dist: mypy>=1.13; extra == 'dev'
26
+ Requires-Dist: pip-audit>=2.7; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=6.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.3; extra == 'dev'
30
+ Requires-Dist: ruff>=0.8; extra == 'dev'
31
+ Requires-Dist: types-markdown>=3.6; extra == 'dev'
32
+ Provides-Extra: ingest
33
+ Requires-Dist: beautifulsoup4>=4.12; extra == 'ingest'
34
+ Requires-Dist: boto3>=1.34; extra == 'ingest'
35
+ Requires-Dist: curl-cffi>=0.15.0; extra == 'ingest'
36
+ Requires-Dist: httpx>=0.27; extra == 'ingest'
37
+ Requires-Dist: pandas>=2.0; extra == 'ingest'
38
+ Requires-Dist: pyarrow>=15.0; extra == 'ingest'
39
+ Requires-Dist: structlog>=24.1; extra == 'ingest'
40
+ Requires-Dist: tenacity>=8.2; extra == 'ingest'
41
+ Requires-Dist: yfinance>=0.2.36; extra == 'ingest'
42
+ Provides-Extra: server
43
+ Requires-Dist: aiosqlite>=0.20; extra == 'server'
44
+ Requires-Dist: alembic>=1.14; extra == 'server'
45
+ Requires-Dist: argon2-cffi>=23.1.0; extra == 'server'
46
+ Requires-Dist: boto3>=1.34; extra == 'server'
47
+ Requires-Dist: cryptography>=46.0.6; extra == 'server'
48
+ Requires-Dist: edgartools>=3.0; extra == 'server'
49
+ Requires-Dist: fastapi>=0.115; extra == 'server'
50
+ Requires-Dist: finnhub-python>=2.4; extra == 'server'
51
+ Requires-Dist: fredapi>=0.5; extra == 'server'
52
+ Requires-Dist: greenlet>=3.0; extra == 'server'
53
+ Requires-Dist: markdown>=3.6; extra == 'server'
54
+ Requires-Dist: mcp>=1.2; extra == 'server'
55
+ Requires-Dist: pyarrow>=15.0; extra == 'server'
56
+ Requires-Dist: pydantic[email]>=2.9; extra == 'server'
57
+ Requires-Dist: pyotp>=2.9; extra == 'server'
58
+ Requires-Dist: qrcode[pil]>=7.4; extra == 'server'
59
+ Requires-Dist: sqlmodel>=0.0.22; extra == 'server'
60
+ Requires-Dist: stripe>=8.0; extra == 'server'
61
+ Requires-Dist: structlog>=24.1; extra == 'server'
62
+ Requires-Dist: uvicorn[standard]>=0.30; extra == 'server'
63
+ Provides-Extra: sms
64
+ Requires-Dist: twilio>=9.0; extra == 'sms'
65
+ Description-Content-Type: text/markdown
66
+
67
+ # Finlet
68
+
69
+ **An evaluation harness for AI trading agents.** Daily-timeframe. US equities. Long-only.
70
+
71
+ Test your AI agent's *reasoning* — not just its algorithms.
72
+
73
+ Finlet is an agent evaluation harness that tests whether AI trading agents make sound decisions with the information available to them. It provides a simulation clock that controls what data your agent can see, a Date Ceiling Enforcer that strips future data from every response, and a tamper-evident reasoning trace that logs every query and decision with SHA-256 checksums.
74
+
75
+ This is not a backtester. Backtesters test strategies against historical data. Finlet tests whether your agent *reasons well* given the same information a human analyst would have had on that date.
76
+
77
+ ## Why Finlet?
78
+
79
+ Most backtesting frameworks test *algorithms*. Finlet tests *reasoning*.
80
+
81
+ As a backtester, Finlet would lose to QuantConnect on every metric — data coverage, asset classes, speed. As an evaluation harness, it has no direct product competitor.
82
+
83
+ - **Date Ceiling Enforcer** — Defense-in-depth runtime enforcement strips any data past the simulation clock. Property-based tests prove no future item passes. No competitor has an equivalent.
84
+ - **SHA-256 Reasoning Trace** — Every query, decision, and order is logged with tamper-evident checksums. No backtester tracks agent reasoning.
85
+ - **Cross-Scenario Leaderboard** — Standardized evaluation with composite scoring across returns, risk, drawdown, reasoning quality, and information efficiency
86
+ - **Real data sources** — S3 Parquet price data, SEC EDGAR filings, FRED economic indicators, Finnhub news — not synthetic datasets
87
+ - **MCP native** — Connect Claude Code (or any MCP client) directly as a trading agent via 16 purpose-built tools
88
+
89
+ ## v1 Scope
90
+
91
+ Finlet v1 is intentionally scoped to daily-timeframe, US-equities, long-only evaluation. These are design choices, not gaps:
92
+
93
+ | Scope Decision | Rationale |
94
+ |---|---|
95
+ | **EOD data only** | Agent evaluation tests reasoning quality, not execution speed |
96
+ | **US equities only** | Deepest data coverage (price, filings, economic, news) |
97
+ | **Long-only** | Isolates reasoning from margin/borrow mechanics |
98
+ | **No partial fills** | Honest about what EOD data supports (no order book depth) |
99
+ | **No live/paper trading** | Evaluation harness, not trading platform |
100
+
101
+ See [docs/V1_SCOPE.md](docs/V1_SCOPE.md) for the complete scope document with rationale.
102
+
103
+ ---
104
+
105
+ ## Quick Start
106
+
107
+ > **For AI Agents:** Visit [finlet.dev/setup](https://finlet.dev/setup) for a structured setup guide with copy-paste configs for REST API, MCP, and CLI. Machine-readable agent index available at [finlet.dev/llms.txt](https://finlet.dev/llms.txt).
108
+
109
+ ### Cloud (finlet.dev)
110
+
111
+ ```bash
112
+ # 1. Sign up at finlet.dev and get your API key
113
+
114
+ # 2. Create a session
115
+ curl -X POST https://finlet.dev/sessions \
116
+ -H "Authorization: Bearer YOUR_API_KEY" \
117
+ -H "Content-Type: application/json" \
118
+ -d '{
119
+ "name": "tech-test",
120
+ "start_time": "2024-01-02T09:30:00Z",
121
+ "initial_capital": 100000,
122
+ "universe": ["AAPL", "GOOGL", "MSFT", "NVDA"]
123
+ }'
124
+
125
+ # 3. Open the dashboard
126
+ open https://finlet.dev
127
+ ```
128
+
129
+ ### Local
130
+
131
+ ```bash
132
+ # Install
133
+ pip install finlet
134
+
135
+ # Start the server
136
+ finlet serve
137
+
138
+ # Create a session
139
+ curl -X POST http://localhost:8000/sessions \
140
+ -H "Content-Type: application/json" \
141
+ -d '{
142
+ "name": "tech-test",
143
+ "start_time": "2024-01-02T09:30:00Z",
144
+ "initial_capital": 100000,
145
+ "universe": ["AAPL", "GOOGL", "MSFT", "NVDA"]
146
+ }'
147
+
148
+ # Open the dashboard
149
+ open http://localhost:8000
150
+ ```
151
+
152
+ ### Connect Claude Code as a Trading Agent
153
+
154
+ Add to your Claude Code MCP config (`.claude.json` or VS Code settings):
155
+
156
+ ```json
157
+ {
158
+ "mcpServers": {
159
+ "finlet": {
160
+ "command": "finlet",
161
+ "args": ["mcp"]
162
+ }
163
+ }
164
+ }
165
+ ```
166
+
167
+ Then tell Claude: *"Use Finlet to analyze AAPL's fundamentals as of January 2024 and decide whether to buy."*
168
+
169
+ ### Connect via REST API
170
+
171
+ Any HTTP client works. Point your agent at the REST API:
172
+
173
+ ```python
174
+ import httpx
175
+
176
+ async with httpx.AsyncClient(base_url="http://localhost:8000") as client:
177
+ # Get current simulation time
178
+ clock = await client.get(f"/sessions/{session_id}/clock")
179
+
180
+ # Query price data (automatically filtered to sim time)
181
+ prices = await client.get(f"/sessions/{session_id}/market/price",
182
+ params={"ticker": "AAPL", "period": "3mo"})
183
+
184
+ # Submit a trade with reasoning
185
+ await client.post(f"/sessions/{session_id}/trade/order", json={
186
+ "side": "BUY",
187
+ "ticker": "AAPL",
188
+ "quantity": 50,
189
+ "order_type": "MARKET",
190
+ "reasoning": "Strong Q4 earnings beat, raising guidance, reasonable P/E"
191
+ })
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Features
197
+
198
+ ### Simulation Clock
199
+
200
+ Three modes to control how time flows:
201
+
202
+ | Mode | Behavior |
203
+ |------|----------|
204
+ | **FROZEN** | Clock is stopped. Agent can make unlimited queries at the current time. |
205
+ | **STEPPING** | Clock advances by a fixed interval when explicitly told to step. |
206
+ | **CONTINUOUS** | Clock advances in real-time at a configurable speed multiplier. |
207
+
208
+ The clock only moves forward when explicitly advanced. No implicit time progression.
209
+
210
+ ### Date Ceiling Enforcer
211
+
212
+ Defense-in-depth protection against future data leakage:
213
+
214
+ - All timestamps in response data must be <= current sim clock time
215
+ - Items with timestamps after the ceiling are **stripped**
216
+ - Items without timestamps are **excluded by default**
217
+ - Strip counts are logged internally but **never exposed to the agent** (that would leak information about future data existence)
218
+
219
+ ### Portfolio Engine
220
+
221
+ Full portfolio tracking with computed metrics:
222
+
223
+ - Cash tracking, position management, P&L (realized + unrealized)
224
+ - Sharpe ratio, max drawdown, win rate
225
+ - Equity curve time series for charting
226
+
227
+ ### Order System
228
+
229
+ Market, limit, and stop orders with full lifecycle tracking:
230
+
231
+ ```
232
+ PENDING -> FILLED | CANCELLED | REJECTED
233
+ ```
234
+
235
+ Each order supports an optional `reasoning` field for trace logging.
236
+
237
+ ### Plugin System
238
+
239
+ Extensible data source architecture. Built-in plugins connect to real financial APIs:
240
+
241
+ | Plugin | Data Type | API Key | Notes |
242
+ |--------|-----------|---------|-------|
243
+ | **PricePlugin** | Price (OHLCV) | None | S3 Parquet-backed historical price data. |
244
+ | **EDGAR** | SEC Filings | None* | 10-K, 10-Q, 8-K, 13-F, Form 4. *Requires User-Agent email. |
245
+ | **FRED** | Economic | Free key | GDP, unemployment, CPI, rates. ALFRED vintage dates. |
246
+ | **Finnhub** | News + Fundamentals | User key | Company news, earnings, fundamentals. 60 calls/min free tier. |
247
+
248
+ ```bash
249
+ # Configure plugin API keys
250
+ finlet plugins add finnhub --api-key=YOUR_KEY
251
+ finlet plugins add fred --api-key=YOUR_KEY
252
+ ```
253
+
254
+ Plugin configuration is stored at `~/.finlet/plugins.json` (never committed to git).
255
+
256
+ ### Reasoning Trace
257
+
258
+ Every agent interaction is logged:
259
+
260
+ - **Action type**: What kind of query or action (price, news, filing, order, etc.)
261
+ - **Sim time + real time**: When it happened in simulation and wall clock
262
+ - **Request params**: What was requested
263
+ - **Response summary**: What was returned
264
+ - **Reasoning**: Agent's own explanation for the action
265
+ - **Latency**: How long the operation took
266
+
267
+ ### Leaderboard
268
+
269
+ Standardized evaluation across 5 scenarios with composite scoring. Agents are scored on:
270
+
271
+ - Portfolio returns vs. benchmark
272
+ - Risk-adjusted returns (Sharpe ratio)
273
+ - Maximum drawdown
274
+ - Reasoning quality
275
+ - Information efficiency (returns per API call)
276
+
277
+ Opt in to data sharing to appear on the public leaderboard and compare your agent against others.
278
+
279
+ ---
280
+
281
+ ## Architecture
282
+
283
+ ```
284
+ +-------------------------------------------------------------+
285
+ | AI Trading Agent |
286
+ | (Claude Code, custom bot, etc.) |
287
+ +----------+------------------------------+--------------------+
288
+ | MCP (stdio) | REST API
289
+ v v
290
+ +-------------------------------------------------------------+
291
+ | Finlet Server |
292
+ | +-----------+ +-----------+ +----------------------+ |
293
+ | | MCP | | FastAPI | | Static Dashboard | |
294
+ | | Server | | Routes | | (HTML/JS/CSS) | |
295
+ | +-----+-----+ +-----+-----+ +----------------------+ |
296
+ | +-------+-------+ |
297
+ | v |
298
+ | +------------------------------------------------------+ |
299
+ | | Session Engine | |
300
+ | | +---------+ +-----------+ +------------------+ | |
301
+ | | | Clock | | Portfolio | | Order Executor | | |
302
+ | | | (frozen) | | Engine | | | | |
303
+ | | +---------+ +-----------+ +------------------+ | |
304
+ | +------------------------+-------------------------------+ |
305
+ | v |
306
+ | +------------------------------------------------------+ |
307
+ | | Date Ceiling Enforcer | |
308
+ | | (strips data after sim clock time) | |
309
+ | +------------------------+-------------------------------+ |
310
+ | v |
311
+ | +------------------------------------------------------+ |
312
+ | | Plugin Registry | |
313
+ | | +----------+ +-------+ +------+ +---------------+ | |
314
+ | | | Price | | EDGAR | | FRED | | Finnhub | | |
315
+ | | |(S3 Parqt)| |(filing| |(econ)| |(news+fundmntl)| | |
316
+ | | +----------+ +-------+ +------+ +---------------+ | |
317
+ | +------------------------------------------------------+ |
318
+ | | |
319
+ | +------------------------v-------------------------------+ |
320
+ | | SQLite (per-session DB) | |
321
+ | | ~/.finlet/sessions/{id}/session.db | |
322
+ | +------------------------------------------------------+ |
323
+ +-------------------------------------------------------------+
324
+ ```
325
+
326
+ ---
327
+
328
+ ## API Reference
329
+
330
+ Base URL: `http://localhost:8000` (local) or `https://finlet.dev` (cloud)
331
+
332
+ ### Sessions
333
+
334
+ | Method | Endpoint | Description |
335
+ |--------|----------|-------------|
336
+ | `POST` | `/sessions` | Create a new session |
337
+ | `GET` | `/sessions` | List all sessions |
338
+ | `GET` | `/sessions/{id}` | Get session state |
339
+ | `DELETE` | `/sessions/{id}` | End a session |
340
+ | `POST` | `/sessions/{id}/configure` | Update session config |
341
+
342
+ ### Clock
343
+
344
+ | Method | Endpoint | Description |
345
+ |--------|----------|-------------|
346
+ | `GET` | `/sessions/{id}/clock` | Current clock state |
347
+ | `POST` | `/sessions/{id}/clock/freeze` | Freeze the clock |
348
+ | `POST` | `/sessions/{id}/clock/step` | Step by interval |
349
+ | `POST` | `/sessions/{id}/clock/step-to` | Step to a specific time |
350
+ | `POST` | `/sessions/{id}/clock/play` | Start continuous mode |
351
+ | `POST` | `/sessions/{id}/clock/stop` | Stop continuous mode |
352
+
353
+ ### Market Data
354
+
355
+ | Method | Endpoint | Description |
356
+ |--------|----------|-------------|
357
+ | `GET` | `/sessions/{id}/market/price` | Price data (OHLCV) |
358
+ | `POST` | `/sessions/{id}/market/search-news` | Search news articles |
359
+ | `GET` | `/sessions/{id}/market/fundamentals` | Company fundamentals |
360
+ | `GET` | `/sessions/{id}/market/filings` | SEC filings |
361
+ | `GET` | `/sessions/{id}/market/economic` | Economic indicators |
362
+
363
+ ### Trading
364
+
365
+ | Method | Endpoint | Description |
366
+ |--------|----------|-------------|
367
+ | `POST` | `/sessions/{id}/trade/order` | Submit an order |
368
+ | `GET` | `/sessions/{id}/trade/orders` | List orders |
369
+ | `GET` | `/sessions/{id}/trade/orders/{oid}` | Order detail |
370
+ | `DELETE` | `/sessions/{id}/trade/orders/{oid}` | Cancel an order |
371
+
372
+ ### Portfolio
373
+
374
+ | Method | Endpoint | Description |
375
+ |--------|----------|-------------|
376
+ | `GET` | `/sessions/{id}/portfolio` | Portfolio state + metrics |
377
+ | `GET` | `/sessions/{id}/portfolio/history` | Equity curve time series |
378
+ | `GET` | `/sessions/{id}/trace` | Reasoning trace log |
379
+
380
+ Full interactive docs at `/docs` when the server is running.
381
+
382
+ ---
383
+
384
+ ## MCP Tools
385
+
386
+ When connected via MCP, these tools are available to your AI agent:
387
+
388
+ | Tool | Description |
389
+ |------|-------------|
390
+ | `get_price_data` | Fetch OHLCV price data for a ticker |
391
+ | `search_news` | Search news articles by keyword/ticker |
392
+ | `get_fundamentals` | Get company financial fundamentals |
393
+ | `get_filings` | Retrieve SEC filings |
394
+ | `get_economic_data` | Get economic indicators (GDP, CPI, etc.) |
395
+ | `submit_order` | Place a buy/sell order |
396
+ | `get_portfolio` | View current portfolio state |
397
+ | `get_sim_time` | Check current simulation time |
398
+ | `advance_time` | Step the simulation clock forward |
399
+ | `freeze_time` | Freeze the simulation clock |
400
+
401
+ ---
402
+
403
+ ## CLI Reference
404
+
405
+ ```bash
406
+ finlet serve # Start API server + dashboard
407
+ finlet session create # Create a new session
408
+ --name TEXT # Session name
409
+ --start TEXT # Start datetime (ISO format)
410
+ --capital FLOAT # Initial capital (default: 100000)
411
+ --universe TEXT # Comma-separated ticker list
412
+ finlet session list # List all sessions
413
+ finlet plugins list # Show plugin status
414
+ finlet plugins add NAME # Configure a plugin
415
+ --api-key TEXT # API key for the plugin
416
+ finlet mcp # Start MCP server (stdio)
417
+ ```
418
+
419
+ ---
420
+
421
+ ## Tech Stack
422
+
423
+ - **Python 3.12+** — Async throughout, type hints everywhere
424
+ - **FastAPI** — REST API with automatic OpenAPI docs
425
+ - **MCP SDK** — Model Context Protocol server for LLM integration
426
+ - **SQLite** — Per-session database via aiosqlite + SQLModel
427
+ - **TradingView Lightweight Charts** — Dashboard charting (MIT, via CDN)
428
+
429
+ ## Contributing
430
+
431
+ 1. Clone the repo
432
+ 2. Install in dev mode: `pip install -e ".[dev]"`
433
+ 3. Run tests: `pytest`
434
+ 4. Follow the code conventions in `CLAUDE.md`
435
+
436
+ Key rules:
437
+ - All datetimes in UTC internally
438
+ - Async functions everywhere — no sync I/O in the hot path
439
+ - Error messages must be specific and actionable
440
+ - Every plugin response goes through the date ceiling enforcer
441
+ - Mock external APIs in tests — no real network calls
442
+
443
+ ## Disclaimers
444
+
445
+ Finlet is provided for educational and research purposes only. It is not financial advice and should not be used as the basis for any investment decisions.
446
+
447
+ Past performance of simulated trading strategies does not guarantee future results. All market data is historical and provided by third-party sources subject to their respective terms of service.
448
+
449
+ Finlet is not affiliated with any stock exchange, broker, or financial institution.
450
+
451
+ ## License
452
+
453
+ Proprietary
@@ -0,0 +1,8 @@
1
+ finlet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ finlet/cli.py,sha256=mJM0dJps3Hq0C2QKgivNQFG5_BrtZ6bNpK3sxFbMhsE,7981
3
+ finlet/config.py,sha256=yHQg_h5GRXJgjsuFX3JzSyV0WSv0OONvLSizZVpdxtI,6548
4
+ finlet-0.0.1.dist-info/METADATA,sha256=LFrXy6f_VnFyinEcwB0oc8xCIeK4lAEIrokLkaqCUO8,17583
5
+ finlet-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ finlet-0.0.1.dist-info/entry_points.txt,sha256=N9XOBMjS1Zw0Q7Fmev4VZNpi-uHulse99woMbsuYBLg,42
7
+ finlet-0.0.1.dist-info/licenses/LICENSE,sha256=Qy1lchoN5yOAoHcIF4ZS9N3suMh-ZTko5IaXndl5d4g,882
8
+ finlet-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ finlet = finlet.cli:app
@@ -0,0 +1,17 @@
1
+ Copyright (c) 2026 Finlet Contributors. All rights reserved.
2
+
3
+ This software and associated documentation files (the "Software") are
4
+ proprietary and confidential. Unauthorized copying, distribution,
5
+ modification, or use of the Software, in whole or in part, via any medium,
6
+ is strictly prohibited without the prior written permission of the copyright
7
+ holder.
8
+
9
+ The Software is provided "AS IS", without warranty of any kind, express or
10
+ implied, including but not limited to the warranties of merchantability,
11
+ fitness for a particular purpose, and noninfringement. In no event shall the
12
+ authors or copyright holders be liable for any claim, damages, or other
13
+ liability, whether in an action of contract, tort, or otherwise, arising
14
+ from, out of, or in connection with the Software or the use or other
15
+ dealings in the Software.
16
+
17
+ For licensing inquiries, contact: [insert contact email]