crowdwisdomtrading 0.2.0__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.
@@ -0,0 +1,5 @@
1
+ """crowdwisdomtrading: client for CrowdWisdom Trading crowd data (free weekly crowd score, x402 pay-per-call or Bearer key)."""
2
+ from crowdwisdomtrading.client import Client, CrowdWisdomError, PaymentFailed
3
+
4
+ __version__ = "0.2.0"
5
+ __all__ = ["Client", "CrowdWisdomError", "PaymentFailed", "__version__"]
@@ -0,0 +1,138 @@
1
+ """CrowdConsensusModel: an ai-hedge-fund AlphaModel that buys CrowdWisdom crowd consensus per call.
2
+
3
+ Register it in hedge_fund/signals/__init__.py:
4
+ from crowdwisdomtrading.aihf import CrowdConsensusModel
5
+ ALPHA_MODEL_REGISTRY["crowd"] = CrowdConsensusModel
6
+ and add it to a strategy YAML (models: - name: crowd). It pays with the wallet in
7
+ X402_WALLET_PRIVATE_KEY (the same one ai-hedge-fund uses for Financial Datasets) or uses a
8
+ CWT_API_KEY Bearer key. predict() only ever asks for data dated on or before `date`
9
+ (point-in-time), and disk caching means one payment per (ticker, date) in a backtest.
10
+
11
+ Works without ai-hedge-fund installed (a small shim replaces Signal / QuantModel) so the
12
+ package can be tested standalone; with aihf installed the real classes are used.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from datetime import date, datetime
17
+ from typing import Any, ClassVar
18
+
19
+ from crowdwisdomtrading.client import Client
20
+
21
+ try: # real ai-hedge-fund classes
22
+ from hedge_fund.models import Signal # type: ignore
23
+ from hedge_fund.signals.base import QuantModel # type: ignore
24
+
25
+ HAVE_AIHF = True
26
+ except Exception: # pragma: no cover - shim path is tested explicitly below
27
+ HAVE_AIHF = False
28
+ from dataclasses import dataclass, field
29
+
30
+ @dataclass
31
+ class Signal: # type: ignore[no-redef]
32
+ model_name: str
33
+ ticker: str
34
+ date: str
35
+ value: float
36
+ reasoning: str | None = None
37
+ components: dict[str, float] = field(default_factory=dict)
38
+ metadata: dict[str, Any] = field(default_factory=dict)
39
+ horizon_days: int | None = None
40
+
41
+ class QuantModel: # type: ignore[no-redef]
42
+ investment_approach: ClassVar[str] = "long_short"
43
+ horizon_days: ClassVar[int] = 5
44
+
45
+
46
+ def _as_day(value: date | str) -> str:
47
+ if isinstance(value, datetime):
48
+ return value.date().isoformat()
49
+ if isinstance(value, date):
50
+ return value.isoformat()
51
+ return str(value)[:10]
52
+
53
+
54
+ class CrowdConsensusModel(QuantModel):
55
+ """Crowd consensus (-1..+1) from YouTube, X and Reddit mentions, as of the backtest date."""
56
+
57
+ investment_approach = "long_short"
58
+ horizon_days = 5
59
+
60
+ def __init__(
61
+ self,
62
+ client: Client | None = None,
63
+ *,
64
+ cycle: str | None = None,
65
+ max_staleness_days: int = 10,
66
+ neutral_band: float = 0.0,
67
+ ) -> None:
68
+ self._client = client
69
+ self._cycle = cycle
70
+ self._max_staleness_days = max_staleness_days
71
+ self._neutral_band = neutral_band
72
+
73
+ @property
74
+ def name(self) -> str:
75
+ return "crowd"
76
+
77
+ @property
78
+ def client(self) -> Client:
79
+ if self._client is None:
80
+ self._client = Client()
81
+ return self._client
82
+
83
+ def predict(self, ticker: str, date: date | str, data_client: Any = None) -> Signal: # noqa: A002
84
+ """Signal for `ticker` using only crowd sessions dated on or before `date`. data_client is unused."""
85
+ day = _as_day(date)
86
+ try:
87
+ answer = self.client.consensus(ticker, as_of=day, cycle=self._cycle)
88
+ except Exception as exc:
89
+ return self._abstain(ticker, day, f"crowd data unavailable: {type(exc).__name__}")
90
+ data = answer.get("data") or {}
91
+ session = data.get("session") or {}
92
+ value = data.get("value")
93
+ if value is None or not session.get("date"):
94
+ return self._abstain(ticker, day, "no crowd session on or before this date")
95
+ try:
96
+ age = (datetime.strptime(day, "%Y-%m-%d").date() - datetime.strptime(str(session["date"]), "%Y-%m-%d").date()).days
97
+ except ValueError:
98
+ return self._abstain(ticker, day, "malformed crowd session date")
99
+ if age > self._max_staleness_days:
100
+ return self._abstain(ticker, day, f"latest crowd session {session['date']} is {age} days old")
101
+ if abs(float(value)) < self._neutral_band:
102
+ value = 0.0
103
+ sources = data.get("sources") or {}
104
+ components = {k: float(v) / 100.0 for k, v in sources.items() if v is not None}
105
+ mentions = data.get("mentions") or {}
106
+ receipt = (answer.get("meta") or {}).get("payment") or {}
107
+ return Signal(
108
+ model_name=self.name,
109
+ ticker=ticker,
110
+ date=day,
111
+ value=max(-1.0, min(1.0, float(value))),
112
+ reasoning=(
113
+ f"Crowd {data.get('direction') or 'view'} {float(data.get('score') or 0):+.0f} on {session['date']} "
114
+ f"({session.get('cycle')}) from {mentions.get('total', '?')} mentions"
115
+ ),
116
+ components=components,
117
+ metadata={
118
+ "as_of": session["date"],
119
+ "cycle": session.get("cycle"),
120
+ "confidence": data.get("confidence"),
121
+ "mentions": mentions,
122
+ "latest_setup": data.get("latest_setup"),
123
+ "payment_tx": receipt.get("transaction"),
124
+ "cached": (answer.get("meta") or {}).get("cached"),
125
+ },
126
+ horizon_days=self.horizon_days,
127
+ )
128
+
129
+ def _abstain(self, ticker: str, day: str, reason: str) -> Signal:
130
+ return Signal(
131
+ model_name=self.name,
132
+ ticker=ticker,
133
+ date=day,
134
+ value=0.0,
135
+ reasoning=reason,
136
+ metadata={"abstained": True},
137
+ horizon_days=self.horizon_days,
138
+ )
@@ -0,0 +1,71 @@
1
+ """crowdwisdomtrading command line: quick checks and demos.
2
+
3
+ crowdwisdomtrading catalog
4
+ crowdwisdomtrading sample
5
+ crowdwisdomtrading daily NVDA (free: daily crowd score)
6
+ crowdwisdomtrading weekly NVDA (free: weekly crowd score)
7
+ crowdwisdomtrading consensus NVDA [--as-of 2026-11-04] [--cycle daily]
8
+ crowdwisdomtrading latest NVDA
9
+ crowdwisdomtrading crowdmap NVDA
10
+ crowdwisdomtrading history NVDA --from 2026-10-01 --to 2026-10-31
11
+ Pays with X402_WALLET_PRIVATE_KEY or uses CWT_API_KEY; --base overrides the API host.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+
19
+ from crowdwisdomtrading.client import Client, CrowdWisdomError
20
+ from crowdwisdomtrading.payment import PaymentFailed
21
+
22
+
23
+ def main(argv: list[str] | None = None) -> int:
24
+ parser = argparse.ArgumentParser(prog="crowdwisdomtrading", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
25
+ parser.add_argument("command", choices=["catalog", "sample", "daily", "weekly", "consensus", "latest", "today", "crowdmap", "history"])
26
+ parser.add_argument("ticker", nargs="?")
27
+ parser.add_argument("--as-of", dest="as_of")
28
+ parser.add_argument("--cycle")
29
+ parser.add_argument("--from", dest="date_from")
30
+ parser.add_argument("--to", dest="date_to")
31
+ parser.add_argument("--base")
32
+ parser.add_argument("--no-cache", action="store_true")
33
+ args = parser.parse_args(argv)
34
+
35
+ client = Client(args.base, cache=not args.no_cache)
36
+ try:
37
+ if args.command == "catalog":
38
+ out = client.catalog()
39
+ elif args.command == "sample":
40
+ out = client.sample()
41
+ elif args.command == "today":
42
+ out = client.setups_today(args.cycle)
43
+ else:
44
+ if not args.ticker:
45
+ parser.error("ticker required")
46
+ if args.command == "daily":
47
+ out = client.daily_sentiment(args.ticker)
48
+ elif args.command == "weekly":
49
+ out = client.weekly_sentiment(args.ticker)
50
+ elif args.command == "consensus":
51
+ out = client.consensus(args.ticker, as_of=args.as_of, cycle=args.cycle)
52
+ elif args.command == "latest":
53
+ out = client.setup_latest(args.ticker, cycle=args.cycle)
54
+ elif args.command == "crowdmap":
55
+ out = client.crowdmap(args.ticker)
56
+ else:
57
+ if not (args.date_from and args.date_to):
58
+ parser.error("--from and --to required")
59
+ out = client.history(args.ticker, args.date_from, args.date_to, cycle=args.cycle)
60
+ except PaymentFailed as exc:
61
+ print(f"payment failed: {exc}", file=sys.stderr)
62
+ return 2
63
+ except CrowdWisdomError as exc:
64
+ print(f"error: {exc}", file=sys.stderr)
65
+ return 1
66
+ print(json.dumps(out, indent=2, default=str))
67
+ return 0
68
+
69
+
70
+ if __name__ == "__main__":
71
+ sys.exit(main())
@@ -0,0 +1,262 @@
1
+ """CrowdWisdom Trading client.
2
+
3
+ Free, no wallet, no key:
4
+ daily_sentiment(ticker) - daily crowd score for one ticker (all sources, newest session)
5
+ weekly_sentiment(ticker) - weekly crowd score for one ticker (all sources, newest Monday session)
6
+ 50 free calls a day per IP address for each.
7
+
8
+ Two ways to the full data:
9
+ wallet mode - Client(wallet_key=...) or env X402_WALLET_PRIVATE_KEY: pays per call on the /x402 routes
10
+ (USDC on Base, same wallet ai-hedge-fund uses for Financial Datasets).
11
+ key mode - Client(api_key=...) or env CWT_API_KEY: a Bearer key for the /v1 routes (monthly plan).
12
+ A key wins over a wallet, so a subscriber is never billed per call.
13
+
14
+ Every method returns {"data": ..., "as_of": ..., "meta": {...}}. Point-in-time: pass as_of / date_to and
15
+ the server only returns what was published by then. Answers with an as_of are cached forever on disk,
16
+ live answers for 10 minutes, so a backtest never pays twice for the same (ticker, date). Errors are
17
+ never cached.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import json
23
+ import os
24
+ import time
25
+ from datetime import date
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ import requests
30
+
31
+ from crowdwisdomtrading.payment import BASE_MAINNET, PaymentFailed, decode_receipt, payment_session
32
+
33
+ DEFAULT_BASE_URL = "https://api.crowdwisdomtrading.com"
34
+ # 2026-11-08: every request names the package, so the API can count package users apart from other callers
35
+ # (stored in cwt_api.api_request_log.client_agent). No personal data, only the package and version.
36
+ LANDING_URL = "https://crowdwisdomtrading.com/x402"
37
+
38
+
39
+ def user_agent() -> str:
40
+ from crowdwisdomtrading import __version__
41
+
42
+ return f"crowdwisdomtrading-python/{__version__} (+{LANDING_URL})"
43
+ DEFAULT_CACHE_DIR = Path.home() / ".crowdwisdomtrading" / "cache"
44
+ LIVE_TTL_S = 600
45
+
46
+
47
+ class CrowdWisdomError(Exception):
48
+ """HTTP or data error from the CrowdWisdom API (status and body excerpt in the message)."""
49
+
50
+ def __init__(self, status: int, message: str) -> None:
51
+ self.status = status
52
+ super().__init__(f"HTTP {status}: {message}")
53
+
54
+
55
+ def _day(value: date | str | None) -> str | None:
56
+ if value is None:
57
+ return None
58
+ if isinstance(value, date):
59
+ return value.isoformat()
60
+ return str(value)[:10]
61
+
62
+
63
+ def _is_past_day(day: str | None) -> bool:
64
+ """Only a finished day is immutable; today's sessions can still be published later today."""
65
+ from datetime import datetime, timezone
66
+
67
+ return bool(day) and day < datetime.now(timezone.utc).date().isoformat()
68
+
69
+
70
+ class Client:
71
+ def __init__(
72
+ self,
73
+ base_url: str | None = None,
74
+ *,
75
+ api_key: str | None = None,
76
+ wallet_key: str | None = None,
77
+ network: str | None = None,
78
+ cache_dir: str | Path | None = None,
79
+ timeout: float = 30.0,
80
+ session: requests.Session | None = None,
81
+ cache: bool = True,
82
+ ) -> None:
83
+ self.base_url = (base_url or os.getenv("CWT_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
84
+ self.api_key = (api_key if api_key is not None else os.getenv("CWT_API_KEY") or "").strip()
85
+ wallet = wallet_key if wallet_key is not None else os.getenv("X402_WALLET_PRIVATE_KEY") or ""
86
+ self._wallet_key = wallet.strip()
87
+ self.network = (network or os.getenv("X402_NETWORK") or BASE_MAINNET).strip()
88
+ self.timeout = timeout
89
+ self.cache_enabled = cache
90
+ self.cache_dir = Path(cache_dir) if cache_dir else Path(os.getenv("CWT_CACHE_DIR") or DEFAULT_CACHE_DIR)
91
+ self._session = session
92
+ self.last_receipt: dict[str, Any] | None = None
93
+
94
+ # ------------------------------------------------------------------ mode
95
+
96
+ @property
97
+ def mode(self) -> str:
98
+ if self.api_key:
99
+ return "key"
100
+ if self._wallet_key:
101
+ return "wallet"
102
+ return "free"
103
+
104
+ def pays_per_call(self) -> bool:
105
+ return self.mode == "wallet"
106
+
107
+ def _http(self) -> requests.Session:
108
+ if self._session is None:
109
+ if self.mode == "wallet":
110
+ self._session = payment_session(
111
+ self._wallet_key, self.network, timeout=max(self.timeout, 60.0), base_url=self.base_url
112
+ )
113
+ else:
114
+ self._session = requests.Session()
115
+ return self._session
116
+
117
+ # ----------------------------------------------------------------- cache
118
+
119
+ def _cache_path(self, key: str) -> Path:
120
+ return self.cache_dir / f"{key}.json"
121
+
122
+ def _cache_get(self, key: str, ttl: float | None) -> dict[str, Any] | None:
123
+ if not self.cache_enabled:
124
+ return None
125
+ path = self._cache_path(key)
126
+ if not path.is_file():
127
+ return None
128
+ try:
129
+ entry = json.loads(path.read_text(encoding="utf-8"))
130
+ except (OSError, ValueError):
131
+ return None
132
+ if ttl is not None and time.time() - float(entry.get("fetched_at", 0)) > ttl:
133
+ return None
134
+ return entry.get("body")
135
+
136
+ def _cache_put(self, key: str, body: dict[str, Any]) -> None:
137
+ if not self.cache_enabled:
138
+ return
139
+ try:
140
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
141
+ self._cache_path(key).write_text(json.dumps({"fetched_at": time.time(), "body": body}), encoding="utf-8")
142
+ except OSError:
143
+ pass
144
+
145
+ # ------------------------------------------------------------------- http
146
+
147
+ def _get(self, path: str, params: dict[str, Any] | None = None, *, point_in_time: bool = False) -> dict[str, Any]:
148
+ params = {k: v for k, v in (params or {}).items() if v is not None}
149
+ key = hashlib.sha256(f"{self.mode}|{self.base_url}|{path}|{json.dumps(params, sort_keys=True)}".encode()).hexdigest()[:24]
150
+ cached = self._cache_get(key, None if point_in_time else LIVE_TTL_S)
151
+ if cached is not None:
152
+ cached.setdefault("meta", {})["cached"] = True
153
+ return cached
154
+ headers = {"Accept": "application/json", "User-Agent": user_agent()}
155
+ if self.mode == "key":
156
+ headers["Authorization"] = f"Bearer {self.api_key}"
157
+ try:
158
+ response = self._http().get(f"{self.base_url}{path}", params=params, headers=headers, timeout=self.timeout)
159
+ except PaymentFailed:
160
+ raise
161
+ except requests.RequestException as exc:
162
+ raise CrowdWisdomError(0, f"{type(exc).__name__}: {exc}") from None
163
+ if response.status_code == 402:
164
+ raise PaymentFailed("payment required: set X402_WALLET_PRIVATE_KEY (with USDC on Base) or CWT_API_KEY")
165
+ if response.status_code >= 400:
166
+ raise CrowdWisdomError(response.status_code, response.text[:300])
167
+ body = response.json()
168
+ if not isinstance(body, dict):
169
+ raise CrowdWisdomError(response.status_code, "unexpected body")
170
+ self.last_receipt = decode_receipt(response)
171
+ body.setdefault("meta", {})
172
+ body["meta"]["payment"] = self.last_receipt
173
+ body["meta"]["cached"] = False
174
+ self._cache_put(key, body)
175
+ return body
176
+
177
+ # ---------------------------------------------------------------- free
178
+
179
+ def catalog(self) -> dict[str, Any]:
180
+ return self._get("/x402/catalog")
181
+
182
+ def sample(self) -> dict[str, Any]:
183
+ return self._get("/x402/sample")
184
+
185
+ def daily_sentiment(self, ticker: str) -> dict[str, Any]:
186
+ """Free daily all-source crowd sentiment score for one ticker (-100..+100, newest session). Never pays, needs
187
+ no key or wallet. Capped per IP per day."""
188
+ t = ticker.upper().strip()
189
+ return self._get(f"/x402/daily/{t}")
190
+
191
+ def weekly_sentiment(self, ticker: str) -> dict[str, Any]:
192
+ """Free weekly all-source crowd sentiment score for one ticker (-100..+100, newest Monday). Never pays, needs
193
+ no key or wallet. Capped per IP per day."""
194
+ t = ticker.upper().strip()
195
+ return self._get(f"/x402/weekly/{t}")
196
+
197
+ # ---------------------------------------------------------------- data
198
+
199
+ def consensus(self, ticker: str, as_of: date | str | None = None, cycle: str | None = None) -> dict[str, Any]:
200
+ """Crowd score in [-1, +1] for one ticker, latest session on or before as_of."""
201
+ t = ticker.upper().strip()
202
+ day = _day(as_of)
203
+ if self.mode == "key":
204
+ body = self._get(f"/v1/sentiment/{t}", point_in_time=False)
205
+ path = [p for p in body.get("path") or [] if day is None or p.get("date", "") <= day]
206
+ if cycle:
207
+ path = [p for p in path if p.get("cycle") == cycle]
208
+ latest = path[-1] if path else None
209
+ score = float(latest["score"]) if latest and latest.get("score") is not None else None
210
+ data = {
211
+ "ticker": t,
212
+ "as_of": day,
213
+ "cycle": cycle,
214
+ "session": {"date": latest["date"], "cycle": latest.get("cycle")} if latest else None,
215
+ "value": max(-1.0, min(1.0, score / 100.0)) if score is not None else None,
216
+ "ProCrowdSentimentScore": score,
217
+ "direction": latest.get("direction") if latest else None,
218
+ "confidence": None,
219
+ "mentions": None,
220
+ "sources": None,
221
+ "latest_setup": None,
222
+ }
223
+ return {"data": data, "as_of": day or (latest["date"] if latest else None), "meta": {**body.get("meta", {}), "route": "/v1/sentiment (key mode)"}}
224
+ return self._get(f"/x402/consensus/{t}", {"as_of": day, "cycle": cycle}, point_in_time=_is_past_day(day))
225
+
226
+ def setup_latest(self, ticker: str, cycle: str | None = None) -> dict[str, Any]:
227
+ t = ticker.upper().strip()
228
+ if self.mode == "key":
229
+ body = self._get("/v1/signals", {"ticker": t, "cycle": cycle, "limit": 100})
230
+ rows = sorted(body.get("data") or [], key=lambda r: (r.get("date") or "", r.get("published_at") or ""))
231
+ row = rows[-1] if rows else None
232
+ return {"data": row, "as_of": row.get("date") if row else None, "meta": {**body.get("meta", {}), "route": "/v1/signals (key mode)"}}
233
+ return self._get(f"/x402/setups/latest/{t}", {"cycle": cycle})
234
+
235
+ def setups_today(self, cycle: str | None = None) -> dict[str, Any]:
236
+ if self.mode == "key":
237
+ body = self._get("/v1/signals", {"cycle": cycle, "limit": 500})
238
+ rows = body.get("data") or []
239
+ latest = max((r.get("date") or "" for r in rows), default=None)
240
+ rows = [r for r in rows if r.get("date") == latest]
241
+ return {"data": {"date": latest, "cycle": cycle, "count": len(rows), "setups": rows}, "as_of": latest, "meta": {**body.get("meta", {}), "route": "/v1/signals (key mode)"}}
242
+ return self._get("/x402/setups/today", {"cycle": cycle})
243
+
244
+ def crowdmap(self, ticker: str) -> dict[str, Any]:
245
+ """CrowdMap: the crowd's support / resistance levels for one ticker (newest day)."""
246
+ t = ticker.upper().strip()
247
+ if self.mode == "key":
248
+ body = self._get(f"/v1/socialmap/{t}", {"days": 1})
249
+ sessions = body.get("sessions") or []
250
+ latest = sessions[-1] if sessions else None
251
+ return {"data": latest, "as_of": latest.get("ladder_date") if latest else None, "meta": {**body.get("meta", {}), "route": "/v1/socialmap (key mode)"}}
252
+ return self._get(f"/x402/crowdmap/{t}")
253
+
254
+ def history(self, ticker: str, date_from: date | str, date_to: date | str, cycle: str | None = None) -> dict[str, Any]:
255
+ """Setups between the two dates. Wallet mode is point-in-time (nothing published after date_to)."""
256
+ t = ticker.upper().strip()
257
+ start, end = _day(date_from), _day(date_to)
258
+ if self.mode == "key":
259
+ body = self._get("/v1/signals", {"ticker": t, "date_from": start, "date_to": end, "cycle": cycle, "limit": 500})
260
+ rows = body.get("data") or []
261
+ return {"data": {"ticker": t, "from": start, "to": end, "count": len(rows), "setups": rows, "point_in_time": False}, "as_of": end, "meta": {**body.get("meta", {}), "route": "/v1/signals (key mode)"}}
262
+ return self._get(f"/x402/setups/history/{t}", {"from": start, "to": end, "cycle": cycle}, point_in_time=_is_past_day(end))
@@ -0,0 +1,91 @@
1
+ """x402 payment session: a requests.Session that pays any 402 it meets, like ai-hedge-fund's wallet.py.
2
+
3
+ The private key never leaves this process and is never logged; errors are re-raised as PaymentFailed
4
+ with the key stripped from the message.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ import requests
11
+
12
+ BASE_MAINNET = "eip155:8453"
13
+ BASE_SEPOLIA = "eip155:84532"
14
+
15
+
16
+ class PaymentFailed(requests.RequestException):
17
+ """The x402 payment could not be made (no funds, facilitator down, wrong network)."""
18
+
19
+
20
+ def _scrub(text: str, secret: str) -> str:
21
+ if not secret:
22
+ return text
23
+ out = text.replace(secret, "<redacted>")
24
+ bare = secret[2:] if secret.lower().startswith("0x") else secret
25
+ return out.replace(bare, "<redacted>") if bare else out
26
+
27
+
28
+ def payment_session(
29
+ wallet_key: str, network: str = BASE_MAINNET, *, timeout: float = 60.0, base_url: str | None = None
30
+ ) -> requests.Session:
31
+ """requests.Session that pays x402 402s. The paying adapter is mounted only on `base_url` (when given) and
32
+ redirects are off, so a 3xx to a third-party 402 can never be paid by accident. Needs crowdwisdomtrading[x402]."""
33
+ try:
34
+ from eth_account import Account
35
+ from x402 import x402ClientSync
36
+ from x402.http.clients.requests import PaymentError, x402HTTPAdapter
37
+ from x402.mechanisms.evm import EthAccountSigner
38
+ from x402.mechanisms.evm.exact import ExactEvmScheme
39
+ except ImportError as exc: # pragma: no cover - exercised only without the extra
40
+ raise ImportError("x402 payments need: pip install 'crowdwisdomtrading[x402]'") from exc
41
+
42
+ client = x402ClientSync()
43
+ client.register(network, ExactEvmScheme(EthAccountSigner(Account.from_key(wallet_key))))
44
+
45
+ class _Adapter(x402HTTPAdapter):
46
+ def send(self, request: Any, **kwargs: Any) -> Any: # type: ignore[override]
47
+ try:
48
+ return super().send(request, **kwargs)
49
+ except PaymentError as exc:
50
+ raise PaymentFailed(_scrub(str(exc), wallet_key)) from None
51
+ except Exception as exc:
52
+ raise PaymentFailed(f"{type(exc).__name__}: {_scrub(str(exc), wallet_key)}") from None
53
+
54
+ session = requests.Session()
55
+ adapter = _Adapter(client)
56
+ if base_url:
57
+ session.mount(base_url.rstrip("/"), adapter)
58
+ else:
59
+ session.mount("https://", adapter)
60
+ session.mount("http://", adapter)
61
+ session.request = _with_defaults(session.request, timeout) # type: ignore[method-assign]
62
+ return session
63
+
64
+
65
+ def _with_defaults(request_fn: Any, timeout: float) -> Any:
66
+ def wrapped(method: str, url: str, **kwargs: Any) -> Any:
67
+ kwargs.setdefault("timeout", timeout)
68
+ kwargs.setdefault("allow_redirects", False)
69
+ return request_fn(method, url, **kwargs)
70
+
71
+ return wrapped
72
+
73
+
74
+ def decode_receipt(response: requests.Response) -> dict[str, Any] | None:
75
+ """The PAYMENT-RESPONSE receipt (tx hash, payer, network) or None when the call was not paid."""
76
+ header = response.headers.get("PAYMENT-RESPONSE")
77
+ if not header:
78
+ return None
79
+ try:
80
+ from x402.http import decode_payment_response_header
81
+
82
+ settled = decode_payment_response_header(header)
83
+ except Exception:
84
+ return None
85
+ return {
86
+ "success": settled.success,
87
+ "transaction": settled.transaction,
88
+ "network": settled.network,
89
+ "payer": settled.payer,
90
+ "amount": settled.amount,
91
+ }
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: crowdwisdomtrading
3
+ Version: 0.2.0
4
+ Summary: Client for CrowdWisdom Trading crowd data: free weekly crowd score, pay per call with x402 or use a Bearer key; includes an ai-hedge-fund alpha model.
5
+ Author: CrowdWisdom Trading
6
+ License: MIT
7
+ Project-URL: Homepage, https://crowdwisdomtrading.com/x402?utm_source=pypi&utm_medium=package&utm_campaign=crowdwisdomtrading
8
+ Project-URL: Catalog, https://api.crowdwisdomtrading.com/x402/catalog
9
+ Project-URL: Docs, https://api.crowdwisdomtrading.com/docs
10
+ Keywords: x402,trading,crowd,sentiment,ai-hedge-fund,alpha
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Topic :: Office/Business :: Financial :: Investment
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: requests>=2.31
17
+ Provides-Extra: x402
18
+ Requires-Dist: x402[evm,requests]>=2.26; extra == "x402"
19
+ Requires-Dist: eth-account>=0.13; extra == "x402"
20
+ Provides-Extra: aihf
21
+ Requires-Dist: aihf>=2.7; extra == "aihf"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8; extra == "dev"
24
+ Requires-Dist: build; extra == "dev"
25
+ Requires-Dist: twine; extra == "dev"
26
+
27
+ # crowdwisdomtrading
28
+
29
+ Python client for [CrowdWisdom Trading](https://crowdwisdomtrading.com/x402?utm_source=pypi&utm_medium=readme&utm_campaign=crowdwisdomtrading) crowd data on US stocks:
30
+ free daily and weekly crowd scores, the per-source crowd consensus, the newest crowd setup (direction, entry,
31
+ targets, stops), today's setups, CrowdMap (crowd support / resistance levels) and point-in-time setup history.
32
+
33
+ Start free with no account. Pay per call with [x402](https://x402.org) (USDC on Base, no account) or use a
34
+ Bearer key from a plan. Includes a ready-made alpha model for [virattt/ai-hedge-fund](https://github.com/virattt/ai-hedge-fund).
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install crowdwisdomtrading # free daily + weekly scores, Bearer-key mode
40
+ pip install "crowdwisdomtrading[x402]" # pay per call with a wallet
41
+ pip install "crowdwisdomtrading[aihf]" # + ai-hedge-fund model
42
+ ```
43
+
44
+ ## Try it free
45
+
46
+ ```python
47
+ from crowdwisdomtrading import Client
48
+
49
+ Client().daily_sentiment("NVDA") # today's crowd score
50
+ Client().weekly_sentiment("NVDA") # this week's
51
+ # {"data": {"cycle": "weekly", "week_of": "2026-11-02", "score": 42.0, "value": 0.42, "direction": "BULLISH",
52
+ # "confidence": 70, "mentions": 380, "sources_included": ["youtube", "x", "reddit"]}, ...}
53
+ ```
54
+
55
+ No key, no wallet. One crowd score from YouTube, X, StockTwits, Reddit, brokers, podcasts and newsletters: of the people
56
+ who took a side, long versus short (-100 to +100). Direction, confidence and the score per source are paid.
57
+ 50 free calls a day per IP address for each (daily and weekly counted separately), any tickers.
58
+
59
+ ## What it costs
60
+
61
+ | Call | Price | What you get |
62
+ |---|---|---|
63
+ | `daily_sentiment(ticker)` | free | Today's `ProCrowdSentimentScore` (-100 to +100) and `ProTraders` (distinct traders behind it; `low_coverage` under 10) |
64
+ | `weekly_sentiment(ticker)` | free | The same for the week (newest Monday) |
65
+ | `consensus(ticker)` | $0.10 | The score per source (YouTube, X, Reddit, brokers, podcasts, newsletters), mention breakdown, point-in-time history (`as_of`) |
66
+ | `setup_latest(ticker)` | $0.50 | Newest setup: direction, entry, 2 targets, 2 stops, confidence, risk/reward |
67
+ | `setups_today()` | $10.00 | Every setup of the newest day |
68
+ | `crowdmap(ticker)` | $0.30 | CrowdMap, the crowd's support / resistance levels, newest day: every level, role, trader count, impact, horizons |
69
+ | `history(ticker, from, to)` | $0.10 | Up to 200 past setups, as published at the time (backtest-safe) |
70
+ | `catalog()`, `sample()` | free | Price list; one setup at least 7 days old |
71
+
72
+ Live prices: `https://api.crowdwisdomtrading.com/x402/catalog`.
73
+
74
+ ## Paying
75
+
76
+ | Mode | Set | What it uses |
77
+ |---|---|---|
78
+ | wallet (pay per call) | `X402_WALLET_PRIVATE_KEY=0x...` with USDC on Base | `/x402/*` routes, receipt (tx hash) on every answer |
79
+ | key (plan) | `CWT_API_KEY=cwt_live_...` | `/v1/*` routes you already pay for monthly |
80
+
81
+ A key wins over a wallet. Fund the wallet with small amounts; the `exact` scheme pays only the quoted
82
+ price and nothing else.
83
+
84
+ ## Use
85
+
86
+ ```python
87
+ from crowdwisdomtrading import Client
88
+
89
+ c = Client() # reads the env vars above
90
+ c.daily_sentiment("NVDA") # free
91
+ c.weekly_sentiment("NVDA") # free
92
+ c.catalog() # free: prices, schemas, examples
93
+ c.sample() # free: one setup at least 7 days old
94
+ c.consensus("NVDA") # {"data": {"value": 0.42, "direction": "BULLISH", ...}, "as_of": ...}
95
+ c.consensus("NVDA", as_of="2026-11-04") # point-in-time: latest session on or before that day
96
+ c.setup_latest("NVDA") # direction, last_close, targets, stops, confidence, rr
97
+ c.setups_today()
98
+ c.crowdmap("NVDA") # CrowdMap: crowd support / resistance levels
99
+ c.history("NVDA", "2026-10-01", "2026-10-31") # only rows published by `to` (backtest-safe)
100
+ c.last_receipt # {"transaction": "0x...", "payer": ..., "network": ...} in wallet mode
101
+ ```
102
+
103
+ Every answer is `{"data": ..., "as_of": ..., "meta": {...}}`. Answers with `as_of` / `to` are cached on disk
104
+ (`~/.crowdwisdomtrading/cache`), live answers for 10 minutes, so a backtest pays once per (ticker, date). Errors
105
+ are never cached. `Client(cache=False)` disables it.
106
+
107
+ ## ai-hedge-fund
108
+
109
+ ```python
110
+ # hedge_fund/signals/__init__.py
111
+ from crowdwisdomtrading.aihf import CrowdConsensusModel
112
+ ALPHA_MODEL_REGISTRY["crowd"] = CrowdConsensusModel
113
+ ```
114
+
115
+ ```yaml
116
+ # hedge_fund/strategies/crowd-tilt.yaml
117
+ name: crowd-tilt
118
+ display_name: Crowd Tilt
119
+ models:
120
+ - name: crowd
121
+ weight: 1.0
122
+ - name: pead
123
+ blend:
124
+ mode: long_short
125
+ method: conviction_weighted
126
+ gross_target: 1.0
127
+ ```
128
+
129
+ `CrowdConsensusModel.predict(ticker, date, data_client)` returns a `Signal` with `value` in [-1, +1]
130
+ (crowd score / 100), `components` per source, and abstains (`value 0`, `metadata.abstained`) when the
131
+ latest crowd session is older than 10 days. It only asks for sessions dated on or before `date`.
132
+
133
+ ## Command line
134
+
135
+ ```bash
136
+ crowdwisdomtrading daily NVDA # free
137
+ crowdwisdomtrading weekly NVDA # free
138
+ crowdwisdomtrading catalog
139
+ crowdwisdomtrading consensus NVDA --as-of 2026-11-04
140
+ crowdwisdomtrading history NVDA --from 2026-10-01 --to 2026-10-31
141
+ ```
142
+
143
+ ## Notes
144
+
145
+ - Data is aggregated crowd opinion for research. Not investment advice. Payments are final; a failed
146
+ response is not charged.
147
+ - Networks: Base mainnet `eip155:8453` (default). `X402_NETWORK=eip155:84532` for Base Sepolia testing.
148
+ - Docs, prices and the paid plans: https://crowdwisdomtrading.com/x402?utm_source=pypi&utm_medium=readme&utm_campaign=crowdwisdomtrading
@@ -0,0 +1,10 @@
1
+ crowdwisdomtrading/__init__.py,sha256=2B1IvdHuB_gfSuESVdELn5JLPLL-aYvbtJuzFig-kyc,307
2
+ crowdwisdomtrading/aihf.py,sha256=Y_bFAi8t3yNpKXq0z3eoorQF3ScuUqqQI_bwpfwaLo8,5529
3
+ crowdwisdomtrading/cli.py,sha256=bNWoXheQxABMPfmnAYuUmHGkxgyzr9lEwi6Qi46FxAU,3015
4
+ crowdwisdomtrading/client.py,sha256=6s2Ru6ReTFY-Wqh8qGczoW4SSbLmzvkZSX-gD6JqnjU,12625
5
+ crowdwisdomtrading/payment.py,sha256=oS3_lo72bVtR70PJkUGb14-aAuj5FS4YXrHdcGbvS9A,3573
6
+ crowdwisdomtrading-0.2.0.dist-info/METADATA,sha256=iITQiUwJUF8LbUAxk5xsxn--PMARs_6J-ApGskHaCg8,7061
7
+ crowdwisdomtrading-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ crowdwisdomtrading-0.2.0.dist-info/entry_points.txt,sha256=2EJMCv_wOu6xXiJI4ECzUGQ_hKYteWfq3qjMUyJRU3E,67
9
+ crowdwisdomtrading-0.2.0.dist-info/top_level.txt,sha256=cMpCt4V8ek9OWd4hZeLvqFq9m5hhoNiL1Cw3d7uvChU,19
10
+ crowdwisdomtrading-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ crowdwisdomtrading = crowdwisdomtrading.cli:main
@@ -0,0 +1 @@
1
+ crowdwisdomtrading