Python SDK for the FinSignals API — Reddit sentiment classification and sector rotation analysis
Project description
FinSignals Python SDK
The official Python client for the FinSignals API.
import finsignals
client = finsignals.Client("fs_your_key_here")
# Reddit sentiment — classify a post across 7 dimensions
result = client.classify(ticker="NVDA", body="Blackwell demand is insane 🚀🚀 DD inside")
print(result.sentiment.label) # "positive"
print(result.directionality.label) # "bullish"
# Sector rotation — daily analysis with 1y and 5y outlooks
rotation = client.get_sector_rotation()
for sector in rotation.outlook_1y.sector_data:
print(sector.name, sector.phase, sector.rotation_score)
Contents
- Install
- Quick start
- Reddit Sentiment Classification
- Sector Rotation Analysis
- Account: usage and plan
- Error handling
- Configuration
- Rate limits
- Code examples in other languages
- Contributing
- License
Install
pip install finsignals-api
Python 3.8+ required. No other non-standard dependencies.
Quick start
Get a free API key at finsignals.ai — 1,000 free credits/month, no credit card required.
Set your key as an environment variable (recommended):
Linux / macOS:
export FINSIGNALS_API_KEY="fs_your_key_here"
Windows (PowerShell):
$env:FINSIGNALS_API_KEY = "fs_your_key_here"
Windows (Command Prompt):
set FINSIGNALS_API_KEY=fs_your_key_here
To set it permanently on Windows, use System Properties → Environment Variables.
Or pass it directly:
client = finsignals.Client(api_key="fs_your_key_here")
Reddit Sentiment Classification
Finance-tuned NLP that classifies Reddit posts and financial text across 7 dimensions in a single API call.
The 7 classification heads
Every /v1/classify call returns all seven signals simultaneously:
| Head | Type | Output |
|---|---|---|
sentiment |
Classification | positive / negative / neutral + probabilities |
directionality |
Classification | bullish / bearish / neutral_direction + probabilities |
quality |
Classification | relevant / noise / spam + probabilities |
post_type |
Classification | dd / news_reaction / technical_analysis / fundamentals / question / general |
relevance_score |
Float [0, 1] | How on-topic is this post for the queried ticker? |
author_confidence |
Float [0, 1] | How confident does the author appear? |
sarcasm |
Boolean | Sarcasm flag (experimental) |
Single classification
result = client.classify(
ticker="AAPL",
title="Apple crushes Q3 earnings",
body="Revenue up 12%, guidance raised. Stock up 5% AH.",
)
print(result.sentiment.label) # "positive"
print(result.sentiment.positive) # 0.92
print(result.directionality.label) # "bullish"
print(result.post_type.label) # "news_reaction"
print(result.relevance_score) # 0.9412
print(result.author_confidence) # 0.71
print(result.sarcasm) # False
print(result.credits_charged) # 1.0
All four fields (ticker, company_name, title, body) are optional — at least one must be non-empty. Including ticker improves relevance scoring.
Batch classification
Send up to 256 posts in a single request. Credit cost: 1.0 + 0.7 × (n - 1) per call.
results = client.classify_batch([
{"ticker": "TSLA", "body": "Delivery miss, stock down premarket."},
{"ticker": "NVDA", "title": "Blackwell demand", "body": "Hyperscaler capex still strong 🚀"},
{"ticker": "AAPL", "body": "Apple beats Q3, guidance raised."},
])
print(results.credits_charged) # 2.4 (1.0 + 0.7 + 0.7)
print(len(results)) # 3
for output in results:
print(output.sentiment.label, output.directionality.label, output.relevance_score)
Output objects are in the same order as your input items. ClassifyBatchResponse supports len(), iteration, and index access (results[0]).
Batch timeouts
classify_batch() automatically computes a longer timeout based on batch size:
max(timeout, 30 + n × 3.0) seconds, giving ~126 s for 32 items and ~798 s for
256 items. You can override it per call:
# Override for a specific large batch
results = client.classify_batch(items, timeout=600)
Full pipeline example
import praw
import finsignals
reddit = praw.Reddit(
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
user_agent="my-scanner/1.0",
)
client = finsignals.Client() # reads FINSIGNALS_API_KEY from env
posts = list(reddit.subreddit("wallstreetbets").hot(limit=100))
items = [
{"ticker": "NVDA", "title": post.title, "body": post.selftext[:1500]}
for post in posts
]
results = client.classify_batch(items)
signals = [
(post, output)
for post, output in zip(posts, results)
if output.quality.label == "relevant"
and output.directionality.label == "bullish"
and output.relevance_score > 0.65
and not output.sarcasm
]
print(f"{len(signals)} signals from {len(posts)} posts")
for post, out in signals:
print(f" [{out.post_type.label}] {post.title[:80]}")
Full tutorial: How to build a Reddit sentiment scanner in Python
Sector Rotation Analysis
GET /v1/sector-rotation delivers a daily market-structure snapshot that shows which sectors and industries are accelerating, peaking, or declining relative to SPY. It is completely independent from the Reddit Sentiment endpoint: different data sources, different calculation pipeline, different response schema.
Cost: 10 credits per call. The report is pre-calculated once per trading day (after midnight ET) and served from cache, so subsequent calls on the same day are fast.
Sector Rotation Quick Example
import finsignals
client = finsignals.Client() # reads FINSIGNALS_API_KEY from env
rotation = client.get_sector_rotation()
print(rotation.trading_date) # "2025-03-24"
print(rotation.credits_charged) # 10.0
# 1-year outlook — sector table
print("=== 1-year outlook ===")
for sector in rotation.outlook_1y.sector_data:
print(
f" {sector.name:<25} phase={sector.phase:<12} "
f"score={sector.rotation_score:+.3f} rs_3m={sector.rs_3m:+.4f}"
)
# 5-year outlook — top industries by rotation score
print("=== 5-year top industries ===")
top5 = sorted(
rotation.outlook_5y.industry_data,
key=lambda x: x.rotation_score or 0,
reverse=True,
)[:5]
for ind in top5:
print(f" {ind.name:<30} parent={ind.parent_sector_etf} score={ind.rotation_score:+.3f}")
# AI-generated markdown summary
print(rotation.outlook_1y.summary_md[:300])
Response structure
client.get_sector_rotation() returns a SectorRotationResponse:
| Field | Type | Description |
|---|---|---|
request_id |
str |
Unique request identifier |
model_version |
str |
API model version string |
credits_charged |
float |
Credits deducted (10.0) |
trading_date |
str |
ISO date the data applies to ("2025-03-24") |
generated_at |
str |
ISO datetime the report was computed |
outlook_1y |
RotationPeriod |
1-year lookback analysis |
outlook_5y |
RotationPeriod |
5-year lookback analysis |
Each RotationPeriod contains:
| Field | Type | Description |
|---|---|---|
trading_date |
str |
Date for this period |
generated_at |
str |
Timestamp this period was computed |
spy_metrics |
SpyMetrics |
SPY benchmark returns (ret_1m … ret_12m) |
sector_data |
List[SectorEntry] |
One entry per sector ETF |
industry_data |
List[IndustryEntry] |
One entry per industry ETF |
summary_md |
Optional[str] |
AI-generated markdown narrative |
weekly_snapshots |
List[dict] |
Historical weekly RS data (raw dicts) |
rs_window_labels |
Optional[dict] |
Display labels for RS columns (5y only) |
Key fields on SectorEntry:
| Field | Type | Description |
|---|---|---|
name |
str |
Sector label ("Technology") |
etf |
str |
Sector ETF ticker ("XLK") |
phase |
str |
Rotation phase (see table below) |
confidence |
float |
Phase classification confidence [0, 1] |
rotation_score |
float |
Composite RS-momentum score |
rs_1m … rs_12m |
float |
Return vs SPY over each window |
mom_accel |
float |
Rate of change in momentum |
vol_ratio |
float |
Sector volatility vs SPY |
pe |
float |
Sector P/E ratio |
Key fields on IndustryEntry (same as SectorEntry plus):
| Field | Type | Description |
|---|---|---|
rs_vs_sector_3m |
float |
Industry RS minus parent sector RS (3M) |
parent_sector_etf |
str |
ETF ticker of the parent sector |
Phase values
| Phase | Meaning |
|---|---|
accumulation |
Early recovery; RS improving from a low base |
advancing |
Sustained outperformance vs SPY |
peaking |
RS still high but momentum decelerating |
distribution |
Outperformance fading; early rotation out |
declining |
Sustained underperformance |
bottoming |
RS low but momentum stabilising |
1-year vs 5-year outlooks
The two outlooks use different RS calculation windows. This is intentional — a 5-year view needs proportionally longer lookback periods to surface structural trends rather than short-term noise.
| RS field | 1-year window | 5-year window |
|---|---|---|
rs_1m |
1 month (~21 bars) | 3 months (~64 bars) |
rs_3m |
3 months (~63 bars) | 6 months (~127 bars) |
rs_6m |
6 months (~126 bars) | 1 year (~252 bars) |
rs_9m |
9 months (~189 bars) | 2 years (~504 bars) |
rs_12m |
12 months (~252 bars) | 3 years (~756 bars) |
outlook_5y.rs_window_labels provides the display mapping:
print(rotation.outlook_5y.rs_window_labels)
# {"rs_1m": "RS 3M", "rs_3m": "RS 6M", "rs_6m": "RS 1Y", "rs_9m": "RS 2Y", "rs_12m": "RS 3Y"}
Handling the not-yet-ready state
The daily calculation runs after midnight ET on trading days. If you call the endpoint before it has run (e.g. very early morning ET), the API returns 503 Service Unavailable. The SDK raises APIError with status_code=503.
import time
import finsignals
from finsignals import APIError
client = finsignals.Client()
for attempt in range(5):
try:
rotation = client.get_sector_rotation()
break
except APIError as e:
if e.status_code == 503:
print(f"Report not yet ready — retrying in 5 minutes (attempt {attempt + 1})")
time.sleep(300)
else:
raise
Sector Rotation API docs
Full REST reference, field descriptions, and interactive examples: finsignals.ai/sector-rotation-api/
The live report is published daily at finsignals.ai/sector-rotation/.
Account: usage and plan
usage = client.get_usage()
print(usage.plan) # "pro"
print(usage.monthly_credits_remaining) # 987499.5
plan = client.get_plan()
print(plan.rate_limits.batch) # 60 (requests/minute)
Error handling
import finsignals
from finsignals import (
AuthenticationError,
InsufficientCreditsError,
RateLimitError,
ValidationError,
BatchTooLargeError,
APIError,
)
try:
result = client.classify(ticker="NVDA", body="test")
except AuthenticationError:
print("Invalid API key — check finsignals.ai/api-keys")
except InsufficientCreditsError as e:
print(f"Out of credits: {e}")
except RateLimitError as e:
print(f"Rate limited — retry after {e.retry_after:.1f}s")
except ValidationError as e:
print(f"Bad request: {e.errors}")
except APIError as e:
if e.status_code == 503:
print("Sector rotation report not yet ready for today — try again later")
else:
print(f"Unexpected error {e.status_code}: {e}")
BatchTooLargeError is raised client-side (before the HTTP request) if you pass more than 256 items to classify_batch().
Configuration
client = finsignals.Client(
api_key="fs_your_key_here", # or set FINSIGNALS_API_KEY env var
timeout=30, # timeout for single/health/usage calls (default: 30 s)
max_retries=2, # retries on 5xx errors (default: 2)
)
For batch calls, classify_batch() overrides timeout automatically based on batch
size unless you pass an explicit timeout to that call (see Batch timeouts above).
Rate limits
Limits are per API key on a 60-second sliding window:
| Plan | /v1/classify (req/min) |
/v1/classify/batch (req/min) |
|---|---|---|
| Free | 5 | 2 |
| Starter | 30 | 15 |
| Pro | 120 | 60 |
| Scale / Enterprise | 600 | 300 |
Your exact limits are always available from client.get_plan():
plan = client.get_plan()
print(plan.rate_limits.single) # e.g. 120
print(plan.rate_limits.batch) # e.g. 60
When exceeded the API returns 429 Too Many Requests; the SDK raises RateLimitError with a retry_after attribute.
Code examples in other languages
cURL — sentiment
curl -sS -X POST "https://api.finsignals.ai/v1/classify" \
-H "X-API-Key: $FINSIGNALS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ticker":"NVDA","body":"Blackwell demand is insane 🚀 DD inside"}' | jq .
cURL — sector rotation
curl -sS "https://api.finsignals.ai/v1/sector-rotation" \
-H "X-API-Key: $FINSIGNALS_API_KEY" | jq '.outlook_1y.sector_data | to_entries[0]'
Node.js (fetch)
const res = await fetch("https://api.finsignals.ai/v1/classify", {
method: "POST",
headers: {
"X-API-Key": process.env.FINSIGNALS_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ ticker: "TSLA", body: "Production ramp on track." }),
});
const data = await res.json();
if (!res.ok) throw new Error(JSON.stringify(data));
console.log(data.outputs[0].directionality.label);
PHP
<?php
$apiKey = getenv('FINSIGNALS_API_KEY');
$payload = json_encode([
'ticker' => 'MSFT',
'body' => 'Azure revenue up 21% YoY, cloud momentum continues.',
]);
$ch = curl_init('https://api.finsignals.ai/v1/classify');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("API error $status: $body");
}
$data = json_decode($body, true);
$out = $data['outputs'][0];
echo $out['sentiment']['label'] . ' / ' . $out['directionality']['label'] . PHP_EOL;
Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
payload, _ := json.Marshal(map[string]string{
"ticker": "AMZN",
"body": "AWS margin expansion drives record operating income.",
})
req, _ := http.NewRequest("POST", "https://api.finsignals.ai/v1/classify", bytes.NewBuffer(payload))
req.Header.Set("X-API-Key", os.Getenv("FINSIGNALS_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
outputs := result["outputs"].([]interface{})
first := outputs[0].(map[string]interface{})
sentiment := first["sentiment"].(map[string]interface{})
fmt.Println(sentiment["label"])
}
Ruby
require 'net/http'
require 'json'
require 'uri'
uri = URI('https://api.finsignals.ai/v1/classify')
payload = { ticker: 'GOOGL', body: 'Search ad revenue rebounds, AI overviews expanding.' }
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 10
http.read_timeout = 30
request = Net::HTTP::Post.new(uri.path)
request['X-API-Key'] = ENV['FINSIGNALS_API_KEY']
request['Content-Type'] = 'application/json'
request.body = payload.to_json
response = http.request(request)
raise "API error #{response.code}: #{response.body}" unless response.code == '200'
data = JSON.parse(response.body)
out = data['outputs'][0]
puts "#{out['sentiment']['label']} | score: #{out['relevance_score']}"
Contributing
Issues and pull requests welcome at github.com/finsignals/finsignals-python.
To run the test suite locally:
git clone https://github.com/finsignals/finsignals-python
cd finsignals-python
pip install -e ".[dev]"
pytest tests/ -v
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file finsignals_api-0.3.0.tar.gz.
File metadata
- Download URL: finsignals_api-0.3.0.tar.gz
- Upload date:
- Size: 23.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0dc00615a36842b9c7845d3b348fdba2e91dad4739e5e6a9890c0c0f20808d5a
|
|
| MD5 |
3eb811f03c03ae08f3da59cc10f03cfc
|
|
| BLAKE2b-256 |
35063fa541f144cddd873560f9eee41e609eb8b46c54abf4412379b41a319e8a
|
File details
Details for the file finsignals_api-0.3.0-py3-none-any.whl.
File metadata
- Download URL: finsignals_api-0.3.0-py3-none-any.whl
- Upload date:
- Size: 20.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f48d7afd27cadcb676bdb9b07030b9eb90d49d5d213da7a6325f7e144401f0f0
|
|
| MD5 |
60d49d216e2cd3ea06f2c8aa3d65bea1
|
|
| BLAKE2b-256 |
9f37b7a21228c7e1bc6a91c96af645faae83f4058ee34f336a0c874d973ef2e6
|