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.
- crowdwisdomtrading/__init__.py +5 -0
- crowdwisdomtrading/aihf.py +138 -0
- crowdwisdomtrading/cli.py +71 -0
- crowdwisdomtrading/client.py +262 -0
- crowdwisdomtrading/payment.py +91 -0
- crowdwisdomtrading-0.2.0.dist-info/METADATA +148 -0
- crowdwisdomtrading-0.2.0.dist-info/RECORD +10 -0
- crowdwisdomtrading-0.2.0.dist-info/WHEEL +5 -0
- crowdwisdomtrading-0.2.0.dist-info/entry_points.txt +2 -0
- crowdwisdomtrading-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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 @@
|
|
|
1
|
+
crowdwisdomtrading
|