opentradex 0.1.0

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.
Files changed (70) hide show
  1. package/.env.example +8 -0
  2. package/CLAUDE.md +98 -0
  3. package/README.md +246 -0
  4. package/SOUL.md +79 -0
  5. package/SPEC.md +317 -0
  6. package/SUBMISSION.md +30 -0
  7. package/architecture.excalidraw +170 -0
  8. package/architecture.png +0 -0
  9. package/bin/opentradex.mjs +4 -0
  10. package/data/.gitkeep +0 -0
  11. package/data/strategy_notes.md +158 -0
  12. package/gossip/__init__.py +0 -0
  13. package/gossip/dashboard.py +150 -0
  14. package/gossip/db.py +358 -0
  15. package/gossip/kalshi.py +492 -0
  16. package/gossip/news.py +235 -0
  17. package/gossip/trader.py +646 -0
  18. package/main.py +287 -0
  19. package/package.json +47 -0
  20. package/requirements.txt +7 -0
  21. package/src/cli.mjs +124 -0
  22. package/src/index.mjs +420 -0
  23. package/web/AGENTS.md +5 -0
  24. package/web/CLAUDE.md +1 -0
  25. package/web/README.md +36 -0
  26. package/web/components.json +25 -0
  27. package/web/eslint.config.mjs +18 -0
  28. package/web/next.config.ts +7 -0
  29. package/web/package-lock.json +11626 -0
  30. package/web/package.json +37 -0
  31. package/web/postcss.config.mjs +7 -0
  32. package/web/public/file.svg +1 -0
  33. package/web/public/globe.svg +1 -0
  34. package/web/public/next.svg +1 -0
  35. package/web/public/vercel.svg +1 -0
  36. package/web/public/window.svg +1 -0
  37. package/web/src/app/api/agent/route.ts +77 -0
  38. package/web/src/app/api/agent/stream/route.ts +87 -0
  39. package/web/src/app/api/markets/route.ts +15 -0
  40. package/web/src/app/api/news/live/route.ts +77 -0
  41. package/web/src/app/api/news/reddit/route.ts +118 -0
  42. package/web/src/app/api/news/route.ts +10 -0
  43. package/web/src/app/api/news/tiktok/route.ts +115 -0
  44. package/web/src/app/api/news/truthsocial/route.ts +116 -0
  45. package/web/src/app/api/news/twitter/route.ts +186 -0
  46. package/web/src/app/api/portfolio/route.ts +50 -0
  47. package/web/src/app/api/prices/route.ts +18 -0
  48. package/web/src/app/api/trades/route.ts +10 -0
  49. package/web/src/app/favicon.ico +0 -0
  50. package/web/src/app/globals.css +170 -0
  51. package/web/src/app/layout.tsx +36 -0
  52. package/web/src/app/page.tsx +366 -0
  53. package/web/src/components/AgentLog.tsx +71 -0
  54. package/web/src/components/LiveStream.tsx +394 -0
  55. package/web/src/components/MarketScanner.tsx +111 -0
  56. package/web/src/components/NewsFeed.tsx +561 -0
  57. package/web/src/components/PortfolioStrip.tsx +139 -0
  58. package/web/src/components/PositionsPanel.tsx +219 -0
  59. package/web/src/components/TopBar.tsx +127 -0
  60. package/web/src/components/ui/badge.tsx +52 -0
  61. package/web/src/components/ui/button.tsx +60 -0
  62. package/web/src/components/ui/card.tsx +103 -0
  63. package/web/src/components/ui/scroll-area.tsx +55 -0
  64. package/web/src/components/ui/separator.tsx +25 -0
  65. package/web/src/components/ui/tabs.tsx +82 -0
  66. package/web/src/components/ui/tooltip.tsx +66 -0
  67. package/web/src/lib/db.ts +81 -0
  68. package/web/src/lib/types.ts +130 -0
  69. package/web/src/lib/utils.ts +6 -0
  70. package/web/tsconfig.json +34 -0
package/main.py ADDED
@@ -0,0 +1,287 @@
1
+ """
2
+ Open Trademaxxxing - agent orchestrator.
3
+
4
+ Spawns Claude Code as a subprocess. The agent reads SOUL.md for personality,
5
+ uses tools directly (web search, Apify, Kalshi API), and persists state to SQLite.
6
+
7
+ Usage:
8
+ python3 main.py # single research + trade cycle
9
+ python3 main.py --loop # continuous loop
10
+ python3 main.py --loop --interval 300 # 5 min interval
11
+ python3 main.py --rationale "I think tariffs will escalate next week"
12
+ python3 main.py --prompt "check my positions and update strategy notes"
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import subprocess
21
+ import sys
22
+ import time
23
+ from datetime import datetime, timezone
24
+ from pathlib import Path
25
+
26
+ from dotenv import load_dotenv
27
+
28
+ load_dotenv(Path(__file__).resolve().parent / ".env")
29
+
30
+ PROJECT_DIR = Path(__file__).resolve().parent
31
+ DATA_DIR = PROJECT_DIR / "data"
32
+ SESSION_FILE = DATA_DIR / "session_id.txt"
33
+
34
+ # The agent prompt: agentic, not scripted. Agent uses its own tools.
35
+ CYCLE_PROMPT = """Read SOUL.md for your identity and strategy principles.
36
+ Read data/strategy_notes.md for lessons from past sessions.
37
+ Check data/user_rationales.json for any pending user theses to research.
38
+
39
+ Then run a full trading cycle:
40
+
41
+ 1. AUTO-SETTLE resolved markets first:
42
+ `PYTHONPATH=. python3 gossip/trader.py check-settled`
43
+ This checks Kalshi for any markets that have resolved and auto-settles them, returning capital to bankroll.
44
+
45
+ 2. Check portfolio and live prices:
46
+ `PYTHONPATH=. python3 gossip/trader.py portfolio`
47
+ `PYTHONPATH=. python3 gossip/trader.py prices`
48
+ Review each open position's unrealized P&L and current market price vs your entry.
49
+
50
+ 3. POSITION REVIEW — for each open position:
51
+ - If current price moved significantly toward your thesis (e.g. YES at 95c+ when you entered at 82c), consider selling now to lock profit vs waiting for settlement.
52
+ - If thesis has weakened or news contradicts it, EXIT: `PYTHONPATH=. python3 gossip/trader.py exit TICKER --reasoning "..."`
53
+ - If thesis still holds and edge remains, HOLD.
54
+ - Use web search to verify — don't just check prices, check if the underlying event happened.
55
+
56
+ 4. MARKET DISCOVERY — use targeted searches, not just broad scans:
57
+ - `PYTHONPATH=. python3 gossip/kalshi.py quick --limit 60` for broad overview
58
+ - `PYTHONPATH=. python3 gossip/kalshi.py search "specific topic"` for targeted lookups
59
+ - Focus on categories where news creates edge: Politics, Economics, Macro events
60
+ - Search for current events you already know about from news/web search
61
+ - Skip sports, entertainment, and illiquid markets (volume < 500, spread > 15c)
62
+
63
+ 5. RESEARCH — pick 3-5 promising markets:
64
+ - Use web search to find relevant news and primary sources
65
+ - Use `PYTHONPATH=. python3 gossip/news.py --keywords "..."` for broader news scraping
66
+ - Estimate the true probability based on evidence
67
+ - Look for near-arbitrage: events that already happened but market hasn't caught up
68
+
69
+ 6. TRADE if you find edge > 10pp with clear reasoning:
70
+ Before executing, answer in one line: "Evidence type: [hard/soft/speculation]. Weakest assumption: [X]."
71
+ If the evidence is speculation, PASS unless edge is overwhelming (>25pp).
72
+ If you can't name what would make you wrong, your thesis isn't specific enough — PASS.
73
+ `PYTHONPATH=. python3 gossip/trader.py trade TICKER --side yes/no --estimate 0.XX --confidence high/medium --reasoning "..."`
74
+
75
+ 7. Update data/strategy_notes.md with what you learned this cycle.
76
+
77
+ EXECUTION DISCIPLINE:
78
+ - Be decisive. Research → conclude → act. Don't loop endlessly.
79
+ - For each market: reach a YES/NO/PASS decision within 2-3 tool calls.
80
+ - Evaluate 3-5 markets per cycle, trade the best 1-2. Don't try to cover everything.
81
+ - If you can't find edge after 5 minutes on a market, pass and move on.
82
+ - Write your conclusion even when you pass — future cycles benefit from it.
83
+ """
84
+
85
+
86
+ def build_rationale_prompt(rationale: str) -> str:
87
+ return f"""Read SOUL.md for your identity and strategy principles.
88
+
89
+ A user has submitted this thesis for you to research and potentially trade on:
90
+
91
+ USER THESIS: {rationale}
92
+
93
+ Your job:
94
+ 1. Research this thesis thoroughly using web search and news scraping.
95
+ 2. Find evidence for AND against.
96
+ 3. Estimate the probability if there's a relevant Kalshi market.
97
+ 4. If you find a market with edge based on this thesis, trade it.
98
+ 5. If the thesis doesn't hold up, explain why and pass.
99
+ 6. Update data/user_rationales.json with your findings.
100
+ 7. Update data/strategy_notes.md if you learned something new.
101
+
102
+ Check portfolio first: `PYTHONPATH=. python3 gossip/trader.py portfolio`
103
+ Scan markets: `PYTHONPATH=. python3 gossip/kalshi.py scan` or `PYTHONPATH=. python3 gossip/kalshi.py search "relevant keywords"`
104
+ """
105
+
106
+
107
+ LIVE_LOG = DATA_DIR / "agent_live.jsonl"
108
+ LIVE_STATUS = DATA_DIR / "agent_status.json"
109
+
110
+
111
+ def write_status(status: str, **extra) -> None:
112
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
113
+ LIVE_STATUS.write_text(json.dumps({"status": status, "timestamp": _now(), **extra}))
114
+
115
+
116
+ def run_agent(prompt: str, timeout: int = 600) -> dict:
117
+ """Spawn Claude Code as a subprocess. Stream output to live log file."""
118
+ cmd = [
119
+ "claude",
120
+ "--print", "-",
121
+ "--output-format", "stream-json",
122
+ "--verbose",
123
+ "--max-turns", "80",
124
+ "--dangerously-skip-permissions",
125
+ ]
126
+
127
+ env = {k: v for k, v in os.environ.items() if k not in {
128
+ "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT",
129
+ }}
130
+ env["PYTHONPATH"] = str(PROJECT_DIR)
131
+
132
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
133
+ # Clear live log for this cycle
134
+ LIVE_LOG.write_text("")
135
+ write_status("running")
136
+
137
+ start = time.time()
138
+ session_id = None
139
+ agent_output = ""
140
+
141
+ try:
142
+ proc = subprocess.Popen(
143
+ cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
144
+ stderr=subprocess.PIPE, text=True, env=env, cwd=str(PROJECT_DIR),
145
+ )
146
+ # Send prompt and close stdin
147
+ proc.stdin.write(prompt)
148
+ proc.stdin.close()
149
+
150
+ # Stream stdout line by line to live log
151
+ with open(LIVE_LOG, "a") as logf:
152
+ for line in proc.stdout:
153
+ line = line.strip()
154
+ if not line:
155
+ continue
156
+ # Write raw line to live log for streaming
157
+ logf.write(line + "\n")
158
+ logf.flush()
159
+
160
+ try:
161
+ msg = json.loads(line)
162
+ if msg.get("type") == "system" and "session_id" in msg:
163
+ session_id = msg["session_id"]
164
+ if msg.get("type") == "result":
165
+ agent_output = msg.get("result", "")
166
+ if msg.get("type") == "assistant" and msg.get("message"):
167
+ for block in msg["message"].get("content", []):
168
+ if isinstance(block, dict) and block.get("type") == "text":
169
+ text = block.get("text", "")
170
+ agent_output += text + "\n"
171
+ write_status("running", last_text=text[:200])
172
+ if msg.get("type") == "assistant" and msg.get("message"):
173
+ for block in msg["message"].get("content", []):
174
+ if isinstance(block, dict) and block.get("type") == "tool_use":
175
+ tool = block.get("name", "")
176
+ write_status("running", tool=tool)
177
+ except json.JSONDecodeError:
178
+ continue
179
+
180
+ if time.time() - start > timeout:
181
+ proc.kill()
182
+ write_status("timeout")
183
+ return {"status": "timeout", "duration_s": timeout, "timestamp": _now()}
184
+
185
+ proc.wait()
186
+
187
+ except Exception as e:
188
+ write_status("error", error=str(e))
189
+ return {"status": "error", "duration_s": round(time.time() - start, 1), "timestamp": _now(), "error": str(e)}
190
+
191
+ duration = round(time.time() - start, 1)
192
+
193
+ cycle_result = {
194
+ "timestamp": _now(),
195
+ "status": "ok" if proc.returncode == 0 else "error",
196
+ "duration_s": duration,
197
+ "session_id": session_id,
198
+ "output": agent_output[:2000] if agent_output else "",
199
+ }
200
+
201
+ write_status("idle", last_cycle=duration)
202
+
203
+ # Log to DB
204
+ try:
205
+ from gossip.db import GossipDB
206
+ db = GossipDB()
207
+ db.log_cycle(
208
+ session_id=session_id or "",
209
+ duration_s=duration,
210
+ status=cycle_result["status"],
211
+ output_summary=agent_output[:1000] if agent_output else "",
212
+ )
213
+ except Exception:
214
+ pass
215
+
216
+ return cycle_result
217
+
218
+
219
+ def submit_rationale(thesis: str) -> None:
220
+ """Add a user rationale to the queue."""
221
+ rationale_file = DATA_DIR / "user_rationales.json"
222
+ data = {"rationales": []}
223
+ if rationale_file.exists():
224
+ try:
225
+ data = json.loads(rationale_file.read_text())
226
+ except Exception:
227
+ pass
228
+
229
+ data["rationales"].append({
230
+ "id": len(data["rationales"]) + 1,
231
+ "timestamp": _now(),
232
+ "thesis": thesis,
233
+ "status": "pending",
234
+ "agent_response": None,
235
+ })
236
+ rationale_file.write_text(json.dumps(data, indent=2))
237
+
238
+
239
+ def _now() -> str:
240
+ return datetime.now(timezone.utc).isoformat()
241
+
242
+
243
+ def main():
244
+ parser = argparse.ArgumentParser(description="Open Trademaxxxing agent")
245
+ parser.add_argument("--loop", action="store_true", help="Run continuously")
246
+ parser.add_argument("--interval", type=int, default=None, help="Cycle interval in seconds")
247
+ parser.add_argument("--prompt", type=str, default=None, help="Custom prompt")
248
+ parser.add_argument("--rationale", type=str, default=None, help="Submit a trading thesis")
249
+ parser.add_argument("--timeout", type=int, default=1200, help="Agent timeout per cycle (default 20min)")
250
+ parser.add_argument("--dry-run", action="store_true")
251
+
252
+ args = parser.parse_args()
253
+ interval = args.interval or int(os.getenv("CYCLE_INTERVAL", "900"))
254
+
255
+ if args.rationale:
256
+ submit_rationale(args.rationale)
257
+ prompt = build_rationale_prompt(args.rationale)
258
+ else:
259
+ prompt = args.prompt or CYCLE_PROMPT
260
+
261
+ if args.dry_run:
262
+ print(prompt)
263
+ return
264
+
265
+ print(f"[Open Trademaxxxing] Starting agent", file=sys.stderr)
266
+ print(f" Mode: {'loop (' + str(interval) + 's)' if args.loop else 'single cycle'}", file=sys.stderr)
267
+
268
+ while True:
269
+ ts = datetime.now(timezone.utc).strftime('%H:%M:%S')
270
+ print(f"\n[{ts}] Starting cycle...", file=sys.stderr)
271
+
272
+ result = run_agent(prompt, timeout=args.timeout)
273
+
274
+ print(f"[{datetime.now(timezone.utc).strftime('%H:%M:%S')}] Done: {result['status']} ({result['duration_s']}s)", file=sys.stderr)
275
+ if result.get("output"):
276
+ print(result["output"][:500], file=sys.stderr)
277
+
278
+ if not args.loop:
279
+ print(json.dumps(result, indent=2))
280
+ break
281
+
282
+ print(f" Next cycle in {interval}s...", file=sys.stderr)
283
+ time.sleep(interval)
284
+
285
+
286
+ if __name__ == "__main__":
287
+ main()
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "opentradex",
3
+ "version": "0.1.0",
4
+ "description": "OpenTradex CLI and onboarding flow for Open Trademaxxxing.",
5
+ "type": "module",
6
+ "bin": {
7
+ "opentradex": "./bin/opentradex.mjs"
8
+ },
9
+ "main": "./src/index.mjs",
10
+ "exports": {
11
+ ".": "./src/index.mjs"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "src/",
16
+ ".env.example",
17
+ "README.md",
18
+ "CLAUDE.md",
19
+ "SOUL.md",
20
+ "SPEC.md",
21
+ "SUBMISSION.md",
22
+ "main.py",
23
+ "requirements.txt",
24
+ "gossip/",
25
+ "data/.gitkeep",
26
+ "data/strategy_notes.md",
27
+ "web/",
28
+ "architecture.excalidraw",
29
+ "architecture.png"
30
+ ],
31
+ "scripts": {
32
+ "opentradex": "node ./bin/opentradex.mjs",
33
+ "test:cli": "node ./bin/opentradex.mjs --help && node ./bin/opentradex.mjs doctor --workspace ."
34
+ },
35
+ "keywords": [
36
+ "kalshi",
37
+ "prediction-markets",
38
+ "trading",
39
+ "agent",
40
+ "claude-code",
41
+ "opentradex"
42
+ ],
43
+ "engines": {
44
+ "node": ">=22.0.0"
45
+ },
46
+ "license": "UNLICENSED"
47
+ }
@@ -0,0 +1,7 @@
1
+ aiohttp>=3.9.0
2
+ apify-client>=1.8.0
3
+ cryptography>=44.0.0
4
+ pandas>=2.2.0
5
+ python-dotenv>=1.0.0
6
+ requests>=2.32.0
7
+ streamlit>=1.40.0
package/src/cli.mjs ADDED
@@ -0,0 +1,124 @@
1
+ import process from "node:process";
2
+ import {
3
+ collectDoctor,
4
+ formatDoctorReport,
5
+ onboard,
6
+ resolveWorkspace,
7
+ runDashboard,
8
+ runSingleCycle,
9
+ runTradingLoop
10
+ } from "./index.mjs";
11
+
12
+ export async function runCli(argv = process.argv.slice(2)) {
13
+ const [command = "help", ...rest] = argv;
14
+ const options = parseArgs(rest);
15
+
16
+ switch (command) {
17
+ case "help":
18
+ case "--help":
19
+ case "-h":
20
+ printHelp();
21
+ return;
22
+ case "onboard": {
23
+ const result = await onboard(options);
24
+ printLines([
25
+ "OpenTradex is ready.",
26
+ `workspace: ${result.workspace}`,
27
+ `mode: ${result.mode}`,
28
+ "",
29
+ "Next commands:",
30
+ " opentradex doctor",
31
+ " opentradex start",
32
+ " opentradex web"
33
+ ]);
34
+ return;
35
+ }
36
+ case "doctor": {
37
+ const report = collectDoctor(options.workspace);
38
+ console.log(formatDoctorReport(report));
39
+ return;
40
+ }
41
+ case "start":
42
+ await runTradingLoop(options);
43
+ return;
44
+ case "cycle":
45
+ await runSingleCycle(options);
46
+ return;
47
+ case "web":
48
+ await runDashboard(options);
49
+ return;
50
+ case "where":
51
+ console.log(resolveWorkspace(options.workspace));
52
+ return;
53
+ default:
54
+ throw new Error(`Unknown opentradex command: ${command}`);
55
+ }
56
+ }
57
+
58
+ function parseArgs(argv) {
59
+ const options = {};
60
+
61
+ for (let index = 0; index < argv.length; index += 1) {
62
+ const token = argv[index];
63
+ if (!token.startsWith("--")) {
64
+ if (!options._) {
65
+ options._ = [];
66
+ }
67
+ options._.push(token);
68
+ continue;
69
+ }
70
+
71
+ const normalized = token.slice(2);
72
+ const [key, explicitValue] = normalized.split("=", 2);
73
+ if (explicitValue !== undefined) {
74
+ options[toCamel(key)] = explicitValue;
75
+ continue;
76
+ }
77
+
78
+ if (["skip-install", "yes", "install", "force", "paper", "live"].includes(key)) {
79
+ options[toCamel(key)] = true;
80
+ continue;
81
+ }
82
+
83
+ const next = argv[index + 1];
84
+ if (!next || next.startsWith("--")) {
85
+ options[toCamel(key)] = true;
86
+ continue;
87
+ }
88
+
89
+ options[toCamel(key)] = next;
90
+ index += 1;
91
+ }
92
+
93
+ return options;
94
+ }
95
+
96
+ function toCamel(value) {
97
+ return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
98
+ }
99
+
100
+ function printHelp() {
101
+ printLines([
102
+ "opentradex",
103
+ "",
104
+ "OpenClaw-style onboarding and runtime wrapper for Open Trademaxxxing.",
105
+ "",
106
+ "Commands:",
107
+ " opentradex onboard [--workspace <dir>] [--paper|--live] [--skip-install]",
108
+ " opentradex doctor [--workspace <dir>]",
109
+ " opentradex start [--workspace <dir>] [--interval <seconds>] [--rationale <text>]",
110
+ " opentradex cycle [--workspace <dir>] [--rationale <text>]",
111
+ " opentradex web [--workspace <dir>] [--install]",
112
+ " opentradex where [--workspace <dir>]",
113
+ "",
114
+ "Examples:",
115
+ " npm install -g .",
116
+ " opentradex onboard",
117
+ " opentradex doctor",
118
+ " opentradex cycle --rationale \"Tariffs will escalate next week\""
119
+ ]);
120
+ }
121
+
122
+ function printLines(lines) {
123
+ console.log(lines.join("\n"));
124
+ }