followsm-sdk 0.1.0__tar.gz

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.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: followsm-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the FollowSM smart-money & orderbook toxicity API
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: requests>=2.31
8
+ Requires-Dist: pydantic>=2.0
9
+ Requires-Dist: websockets>=12.0
10
+ Requires-Dist: pytest>=8.0 ; extra == "test"
11
+ Requires-Dist: pytest-mock>=3.12 ; extra == "test"
12
+ Project-URL: Documentation, https://follow-sm.com/docs
13
+ Project-URL: Homepage, https://follow-sm.com
14
+ Provides-Extra: test
15
+
16
+ # followsm-sdk
17
+
18
+ Official Python SDK for the [FollowSM](https://follow-sm.com) smart-money & orderbook toxicity API.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install followsm-sdk
24
+ ```
25
+
26
+ ## Quickstart
27
+
28
+ ```python
29
+ from followsm_sdk import FollowSMClient
30
+
31
+ client = FollowSMClient(api_key="fsm_live_...") # or omit to use the free tier
32
+ snapshot = client.get_toxicity_snapshot("BTCUSDT")
33
+ print(snapshot.vpin, snapshot.is_toxic_alert)
34
+ ```
35
+
36
+ ## Free vs Developer API
37
+
38
+ | | Free / Unauthenticated | DEVELOPER_API ($199/mo) |
39
+ |---|---|---|
40
+ | Requests/min | 30 | 300 |
41
+ | WebSocket connections | 1 | Multiple |
42
+ | Binance pairs | Limited | 50+ |
43
+ | Latency | Standard | Sub-10ms in-memory snapshots |
44
+
45
+ On HTTP 429, the SDK raises `RateLimitExceededException` with an upgrade prompt pointing to https://follow-sm.com/pricing.
46
+
@@ -0,0 +1,30 @@
1
+ # followsm-sdk
2
+
3
+ Official Python SDK for the [FollowSM](https://follow-sm.com) smart-money & orderbook toxicity API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install followsm-sdk
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ from followsm_sdk import FollowSMClient
15
+
16
+ client = FollowSMClient(api_key="fsm_live_...") # or omit to use the free tier
17
+ snapshot = client.get_toxicity_snapshot("BTCUSDT")
18
+ print(snapshot.vpin, snapshot.is_toxic_alert)
19
+ ```
20
+
21
+ ## Free vs Developer API
22
+
23
+ | | Free / Unauthenticated | DEVELOPER_API ($199/mo) |
24
+ |---|---|---|
25
+ | Requests/min | 30 | 300 |
26
+ | WebSocket connections | 1 | Multiple |
27
+ | Binance pairs | Limited | 50+ |
28
+ | Latency | Standard | Sub-10ms in-memory snapshots |
29
+
30
+ On HTTP 429, the SDK raises `RateLimitExceededException` with an upgrade prompt pointing to https://follow-sm.com/pricing.
@@ -0,0 +1,16 @@
1
+ """followsm-sdk — official Python client for the FollowSM API."""
2
+
3
+ from .client import FollowSMClient
4
+ from .exceptions import AuthenticationError, FollowSMError, RateLimitExceededException
5
+ from .models import OrderBookDepthBand, SymbolToxicityMetrics
6
+
7
+ __all__ = [
8
+ "FollowSMClient",
9
+ "FollowSMError",
10
+ "RateLimitExceededException",
11
+ "AuthenticationError",
12
+ "SymbolToxicityMetrics",
13
+ "OrderBookDepthBand",
14
+ ]
15
+
16
+ __version__ = "0.1.0"
@@ -0,0 +1,79 @@
1
+ """Core synchronous + async streaming client for the FollowSM API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import AsyncIterator, List, Optional
7
+
8
+ import requests
9
+ import websockets
10
+
11
+ from .exceptions import AuthenticationError, RateLimitExceededException
12
+ from .models import SymbolToxicityMetrics
13
+
14
+ DEFAULT_BASE_URL = "https://follow-sm.com/api/v1"
15
+ DEFAULT_WS_URL = "wss://follow-sm.com/api/v1/developer/toxicity/stream"
16
+ FREE_COMMUNITY_KEY = "FREE_COMMUNITY_KEY"
17
+
18
+
19
+ class FollowSMClient:
20
+ """Client for the FollowSM Developer API.
21
+
22
+ Falls back to the shared FREE_COMMUNITY_KEY (30 req/min, unauthenticated
23
+ tier) when no `api_key` is supplied.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ api_key: Optional[str] = None,
29
+ base_url: str = DEFAULT_BASE_URL,
30
+ ws_url: str = DEFAULT_WS_URL,
31
+ timeout: float = 10.0,
32
+ ):
33
+ self.api_key = api_key or FREE_COMMUNITY_KEY
34
+ self.base_url = base_url.rstrip("/")
35
+ self.ws_url = ws_url
36
+ self.timeout = timeout
37
+
38
+ def _headers(self) -> dict:
39
+ return {"X-FollowSM-Key": self.api_key}
40
+
41
+ def _handle_response(self, response: requests.Response) -> requests.Response:
42
+ if response.status_code == 429:
43
+ reset_time = int(response.headers.get("X-RateLimit-Reset", 0))
44
+ raise RateLimitExceededException(
45
+ response.json().get("detail", "Rate limit exceeded")
46
+ if response.content
47
+ else "Rate limit exceeded",
48
+ reset_time,
49
+ )
50
+ if response.status_code in (401, 403):
51
+ raise AuthenticationError(response.text)
52
+ response.raise_for_status()
53
+ return response
54
+
55
+ def get_toxicity_snapshot(self, symbol: str) -> SymbolToxicityMetrics:
56
+ response = requests.get(
57
+ f"{self.base_url}/developer/toxicity/snapshot",
58
+ params={"symbol": symbol},
59
+ headers=self._headers(),
60
+ timeout=self.timeout,
61
+ )
62
+ self._handle_response(response)
63
+ return SymbolToxicityMetrics.model_validate(response.json())
64
+
65
+ def get_toxic_pairs(self) -> List[SymbolToxicityMetrics]:
66
+ response = requests.get(
67
+ f"{self.base_url}/developer/toxicity/toxic-pairs",
68
+ headers=self._headers(),
69
+ timeout=self.timeout,
70
+ )
71
+ self._handle_response(response)
72
+ return [SymbolToxicityMetrics.model_validate(item) for item in response.json()]
73
+
74
+ async def stream_toxicity(self) -> AsyncIterator[SymbolToxicityMetrics]:
75
+ """Async-iterate live toxicity snapshots over the WebSocket feed."""
76
+ uri = f"{self.ws_url}?api_key={self.api_key}"
77
+ async with websockets.connect(uri) as ws:
78
+ async for message in ws:
79
+ yield SymbolToxicityMetrics.model_validate(json.loads(message))
@@ -0,0 +1,22 @@
1
+ """Custom exceptions raised by the FollowSM SDK."""
2
+
3
+
4
+ class FollowSMError(Exception):
5
+ """Base class for all FollowSM SDK errors."""
6
+
7
+
8
+ class RateLimitExceededException(FollowSMError):
9
+ def __init__(self, message: str, reset_time: int):
10
+ self.reset_time = reset_time
11
+ upgrade_url = "https://follow-sm.com/pricing"
12
+ formatted_msg = (
13
+ f"\n\033[91m[FollowSM RateLimitExceeded]\033[0m {message}\n"
14
+ f"\033[93m⚡️ Free Tier Limit Reached (30 req/min).\033[0m\n"
15
+ f"Unlock 300 req/min, 50+ Binance pairs & sub-10ms latency:\n"
16
+ f"👉 \033[94m{upgrade_url}\033[0m\n"
17
+ )
18
+ super().__init__(formatted_msg)
19
+
20
+
21
+ class AuthenticationError(FollowSMError):
22
+ """Raised on HTTP 401/403 responses (invalid key or insufficient plan)."""
@@ -0,0 +1,27 @@
1
+ """Pydantic data models mirroring backend/models/toxicity_models.py."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict
6
+
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class OrderBookDepthBand(BaseModel):
11
+ bid_notional: float
12
+ ask_notional: float
13
+ imbalance_ratio: float
14
+
15
+
16
+ class SymbolToxicityMetrics(BaseModel):
17
+ symbol: str
18
+ timestamp: float
19
+ price: float
20
+ vpin: float
21
+ ob_toxicity_1pct: float
22
+ ob_imbalance_l1: float
23
+ depth_bands: Dict[str, OrderBookDepthBand]
24
+ volume_z_score: float
25
+ natr_15m: float
26
+ taker_buy_ratio: float
27
+ is_toxic_alert: bool
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["flit_core>=3.9,<4"]
3
+ build-backend = "flit_core.buildapi"
4
+
5
+ [project]
6
+ name = "followsm-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the FollowSM smart-money & orderbook toxicity API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ dependencies = [
13
+ "requests>=2.31",
14
+ "pydantic>=2.0",
15
+ "websockets>=12.0",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ test = ["pytest>=8.0", "pytest-mock>=3.12"]
20
+
21
+ [project.urls]
22
+ Homepage = "https://follow-sm.com"
23
+ Documentation = "https://follow-sm.com/docs"