Metadata-Version: 2.4
Name: finsignals-api
Version: 0.2.5
Summary: Python SDK for the FinSignals financial sentiment classification API
Project-URL: Homepage, https://finsignals.ai
Project-URL: Documentation, https://finsignals.ai/docs
Project-URL: Repository, https://github.com/finsignals/finsignals-python
Project-URL: Bug Tracker, https://github.com/finsignals/finsignals-python/issues
License: MIT
License-File: LICENSE
Keywords: finance,financial-nlp,nlp,reddit,sentiment,sentiment-analysis,trading
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.8
Requires-Dist: requests>=2.28.0
Requires-Dist: urllib3>=1.26.0
Provides-Extra: dev
Requires-Dist: black; extra == 'dev'
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: responses>=0.23; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Requires-Dist: types-requests; extra == 'dev'
Description-Content-Type: text/markdown

# FinSignals Python SDK

[![PyPI version](https://badge.fury.io/py/finsignals-api.svg)](https://pypi.org/project/finsignals-api/)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/)

The official Python client for the [FinSignals API](https://finsignals.ai) — a finance-tuned NLP API that classifies Reddit posts and financial text across 7 dimensions in a single call.

```python
import finsignals

client = finsignals.Client("fs_your_key_here")

result = client.classify(ticker="NVDA", body="Blackwell demand is insane 🚀🚀 DD inside")

print(result.sentiment.label)       # "positive"
print(result.directionality.label)  # "bullish"
print(result.quality.label)         # "relevant"
print(result.relevance_score)       # 0.9137
print(result.sarcasm)               # False
print(result.credits_charged)       # 1.0
```

## Install

```bash
pip install finsignals-api
```

Python 3.8+ required. No other non-standard dependencies.

## Quick start

**Get a free API key** at [finsignals.ai](https://finsignals.ai) — 1,000 free credits/month, no credit card required.

Set your key as an environment variable (recommended):

**Linux / macOS:**
```bash
export FINSIGNALS_API_KEY="fs_your_key_here"
```

**Windows (PowerShell):**
```powershell
$env:FINSIGNALS_API_KEY = "fs_your_key_here"
```

**Windows (Command Prompt):**
```cmd
set FINSIGNALS_API_KEY=fs_your_key_here
```

To set it permanently on Windows, use System Properties → Environment Variables.

Or pass it directly:

```python
client = finsignals.Client(api_key="fs_your_key_here")
```

## The 7 classification heads

Every API 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

```python
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.

```python
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 × 1.5)` seconds, giving ~78 s for 32 items and ~414 s for
256 items. You can override it per call:

```python
# Override for a specific large batch
results = client.classify_batch(items, timeout=600)
```

## Full pipeline example (Reddit → FinSignals)

```python
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](https://finsignals.ai/blog/)

## Usage and plan

```python
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

```python
import finsignals
from finsignals import (
    AuthenticationError,
    InsufficientCreditsError,
    RateLimitError,
    ValidationError,
    BatchTooLargeError,
)

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 finsignals.APIError as e:
    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

```python
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](#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()`:

```python
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

```bash
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 .
```

### Node.js (fetch)

```javascript
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
<?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

```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

```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](https://github.com/finsignals/finsignals-python).

To run the test suite locally:

```bash
git clone https://github.com/finsignals/finsignals-python
cd finsignals-python
pip install -e ".[dev]"
pytest tests/ -v
```

## License

MIT
