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.
- package/.env.example +8 -0
- package/CLAUDE.md +98 -0
- package/README.md +246 -0
- package/SOUL.md +79 -0
- package/SPEC.md +317 -0
- package/SUBMISSION.md +30 -0
- package/architecture.excalidraw +170 -0
- package/architecture.png +0 -0
- package/bin/opentradex.mjs +4 -0
- package/data/.gitkeep +0 -0
- package/data/strategy_notes.md +158 -0
- package/gossip/__init__.py +0 -0
- package/gossip/dashboard.py +150 -0
- package/gossip/db.py +358 -0
- package/gossip/kalshi.py +492 -0
- package/gossip/news.py +235 -0
- package/gossip/trader.py +646 -0
- package/main.py +287 -0
- package/package.json +47 -0
- package/requirements.txt +7 -0
- package/src/cli.mjs +124 -0
- package/src/index.mjs +420 -0
- package/web/AGENTS.md +5 -0
- package/web/CLAUDE.md +1 -0
- package/web/README.md +36 -0
- package/web/components.json +25 -0
- package/web/eslint.config.mjs +18 -0
- package/web/next.config.ts +7 -0
- package/web/package-lock.json +11626 -0
- package/web/package.json +37 -0
- package/web/postcss.config.mjs +7 -0
- package/web/public/file.svg +1 -0
- package/web/public/globe.svg +1 -0
- package/web/public/next.svg +1 -0
- package/web/public/vercel.svg +1 -0
- package/web/public/window.svg +1 -0
- package/web/src/app/api/agent/route.ts +77 -0
- package/web/src/app/api/agent/stream/route.ts +87 -0
- package/web/src/app/api/markets/route.ts +15 -0
- package/web/src/app/api/news/live/route.ts +77 -0
- package/web/src/app/api/news/reddit/route.ts +118 -0
- package/web/src/app/api/news/route.ts +10 -0
- package/web/src/app/api/news/tiktok/route.ts +115 -0
- package/web/src/app/api/news/truthsocial/route.ts +116 -0
- package/web/src/app/api/news/twitter/route.ts +186 -0
- package/web/src/app/api/portfolio/route.ts +50 -0
- package/web/src/app/api/prices/route.ts +18 -0
- package/web/src/app/api/trades/route.ts +10 -0
- package/web/src/app/favicon.ico +0 -0
- package/web/src/app/globals.css +170 -0
- package/web/src/app/layout.tsx +36 -0
- package/web/src/app/page.tsx +366 -0
- package/web/src/components/AgentLog.tsx +71 -0
- package/web/src/components/LiveStream.tsx +394 -0
- package/web/src/components/MarketScanner.tsx +111 -0
- package/web/src/components/NewsFeed.tsx +561 -0
- package/web/src/components/PortfolioStrip.tsx +139 -0
- package/web/src/components/PositionsPanel.tsx +219 -0
- package/web/src/components/TopBar.tsx +127 -0
- package/web/src/components/ui/badge.tsx +52 -0
- package/web/src/components/ui/button.tsx +60 -0
- package/web/src/components/ui/card.tsx +103 -0
- package/web/src/components/ui/scroll-area.tsx +55 -0
- package/web/src/components/ui/separator.tsx +25 -0
- package/web/src/components/ui/tabs.tsx +82 -0
- package/web/src/components/ui/tooltip.tsx +66 -0
- package/web/src/lib/db.ts +81 -0
- package/web/src/lib/types.ts +130 -0
- package/web/src/lib/utils.ts +6 -0
- package/web/tsconfig.json +34 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Open Trademaxxxing - Streamlit dashboard.
|
|
3
|
+
|
|
4
|
+
Run: streamlit run gossip/dashboard.py
|
|
5
|
+
|
|
6
|
+
Real-time view of: portfolio, open positions, trade history,
|
|
7
|
+
news feed, market snapshots, and agent cycle logs.
|
|
8
|
+
All data from SQLite (data/gossip.db).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import streamlit as st
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
19
|
+
from gossip.db import GossipDB
|
|
20
|
+
|
|
21
|
+
st.set_page_config(page_title="Open Trademaxxxing", page_icon="📰", layout="wide")
|
|
22
|
+
|
|
23
|
+
@st.cache_resource
|
|
24
|
+
def get_db():
|
|
25
|
+
return GossipDB()
|
|
26
|
+
|
|
27
|
+
db = get_db()
|
|
28
|
+
|
|
29
|
+
st.title("Open Trademaxxxing")
|
|
30
|
+
st.caption("Autonomous prediction market agent — Kalshi")
|
|
31
|
+
|
|
32
|
+
# --- Portfolio metrics ---
|
|
33
|
+
|
|
34
|
+
stats = db.get_stats()
|
|
35
|
+
|
|
36
|
+
col1, col2, col3, col4, col5, col6 = st.columns(6)
|
|
37
|
+
col1.metric("Bankroll", f"${stats['bankroll']:.2f}")
|
|
38
|
+
col2.metric("Total P&L", f"${stats['total_pnl']:+.2f}")
|
|
39
|
+
col3.metric("Trades", stats["total_trades"])
|
|
40
|
+
col4.metric("Win Rate", f"{stats['win_rate']:.0%}" if stats["total_trades"] > 0 else "—")
|
|
41
|
+
col5.metric("Open Positions", stats["open_positions"])
|
|
42
|
+
col6.metric("Agent Cycles", stats["total_agent_cycles"])
|
|
43
|
+
|
|
44
|
+
st.divider()
|
|
45
|
+
|
|
46
|
+
# --- Tabs ---
|
|
47
|
+
|
|
48
|
+
tab_positions, tab_history, tab_news, tab_markets, tab_agent = st.tabs([
|
|
49
|
+
"Open Positions", "Trade History", "News Feed", "Market Snapshots", "Agent Log"
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
# --- Open Positions ---
|
|
53
|
+
|
|
54
|
+
with tab_positions:
|
|
55
|
+
positions = db.get_open_positions()
|
|
56
|
+
if positions:
|
|
57
|
+
for p in positions:
|
|
58
|
+
with st.container():
|
|
59
|
+
c1, c2, c3, c4, c5 = st.columns([3, 1, 1, 1, 1])
|
|
60
|
+
c1.write(f"**{p['ticker']}**")
|
|
61
|
+
c2.write(f"{p['side'].upper()} x{p['contracts']}")
|
|
62
|
+
c3.write(f"Entry: ${p['entry_price']:.2f}")
|
|
63
|
+
c4.write(f"Edge: {p['edge']:+.1%}" if p['edge'] else "—")
|
|
64
|
+
c5.write(f"{p['confidence']}" if p.get('confidence') else "—")
|
|
65
|
+
if p.get("title"):
|
|
66
|
+
st.caption(p["title"])
|
|
67
|
+
if p.get("reasoning"):
|
|
68
|
+
with st.expander("Reasoning"):
|
|
69
|
+
st.write(p["reasoning"])
|
|
70
|
+
st.divider()
|
|
71
|
+
else:
|
|
72
|
+
st.info("No open positions")
|
|
73
|
+
|
|
74
|
+
# --- Trade History ---
|
|
75
|
+
|
|
76
|
+
with tab_history:
|
|
77
|
+
trades = db.get_trade_history(limit=30)
|
|
78
|
+
if trades:
|
|
79
|
+
for t in trades:
|
|
80
|
+
icon = "✅" if t.get("outcome") == "win" else ("❌" if t.get("outcome") == "loss" else "⏳")
|
|
81
|
+
pnl_str = f"P&L: ${t.get('pnl', 0):+.2f}" if t.get("settled") else "OPEN"
|
|
82
|
+
st.write(f"{icon} **{t['ticker']}** — {t['side'].upper()} x{t['contracts']} @ ${t['entry_price']:.2f} → {pnl_str}")
|
|
83
|
+
if t.get("reasoning"):
|
|
84
|
+
with st.expander("Details"):
|
|
85
|
+
st.write(f"**Reasoning:** {t['reasoning']}")
|
|
86
|
+
if t.get("news_trigger"):
|
|
87
|
+
st.write(f"**News trigger:** {t['news_trigger']}")
|
|
88
|
+
st.write(f"**Confidence:** {t.get('confidence', '?')} | **Edge:** {t.get('edge', 0):+.1%}")
|
|
89
|
+
st.write(f"**Time:** {t['timestamp'][:19]}")
|
|
90
|
+
else:
|
|
91
|
+
st.info("No trades yet")
|
|
92
|
+
|
|
93
|
+
# --- News Feed ---
|
|
94
|
+
|
|
95
|
+
with tab_news:
|
|
96
|
+
news = db.get_recent_news(limit=50)
|
|
97
|
+
if news:
|
|
98
|
+
st.write(f"**{len(news)} recent articles**")
|
|
99
|
+
for n in news:
|
|
100
|
+
source_badge = f"`{n.get('source', '?')}`"
|
|
101
|
+
keyword_badge = f"`{n.get('keyword', '')}`" if n.get("keyword") else ""
|
|
102
|
+
title = n.get("title", "Untitled")
|
|
103
|
+
url = n.get("url", "")
|
|
104
|
+
snippet = n.get("snippet", "")
|
|
105
|
+
|
|
106
|
+
if url:
|
|
107
|
+
st.write(f"{source_badge} {keyword_badge} [{title}]({url})")
|
|
108
|
+
else:
|
|
109
|
+
st.write(f"{source_badge} {keyword_badge} {title}")
|
|
110
|
+
if snippet:
|
|
111
|
+
st.caption(snippet[:200])
|
|
112
|
+
else:
|
|
113
|
+
st.info("No news scraped yet")
|
|
114
|
+
|
|
115
|
+
# --- Market Snapshots ---
|
|
116
|
+
|
|
117
|
+
with tab_markets:
|
|
118
|
+
snapshots = db.get_latest_snapshots(limit=30)
|
|
119
|
+
if snapshots:
|
|
120
|
+
import pandas as pd
|
|
121
|
+
df = pd.DataFrame(snapshots)
|
|
122
|
+
cols_to_show = ["ticker", "title", "category", "yes_bid", "yes_ask", "mid", "volume", "open_interest"]
|
|
123
|
+
available_cols = [c for c in cols_to_show if c in df.columns]
|
|
124
|
+
st.dataframe(df[available_cols], use_container_width=True, hide_index=True)
|
|
125
|
+
else:
|
|
126
|
+
st.info("No market snapshots yet — run a scan first")
|
|
127
|
+
|
|
128
|
+
# --- Agent Log ---
|
|
129
|
+
|
|
130
|
+
with tab_agent:
|
|
131
|
+
cycles = db.get_recent_cycles(limit=20)
|
|
132
|
+
if cycles:
|
|
133
|
+
for c in cycles:
|
|
134
|
+
status_icon = "🟢" if c.get("status") == "ok" else "🔴"
|
|
135
|
+
ts = (c.get("timestamp") or "")[:19]
|
|
136
|
+
dur = c.get("duration_s", "?")
|
|
137
|
+
st.write(f"{status_icon} **{ts}** — {dur}s | Markets: {c.get('markets_scanned', 0)} | News: {c.get('news_scraped', 0)} | Trades: {c.get('trades_made', 0)}")
|
|
138
|
+
if c.get("output_summary"):
|
|
139
|
+
with st.expander("Output"):
|
|
140
|
+
st.write(c["output_summary"][:500])
|
|
141
|
+
else:
|
|
142
|
+
st.info("No agent cycles yet")
|
|
143
|
+
|
|
144
|
+
# --- Refresh ---
|
|
145
|
+
|
|
146
|
+
st.sidebar.button("🔄 Refresh", use_container_width=True)
|
|
147
|
+
st.sidebar.divider()
|
|
148
|
+
st.sidebar.write(f"**DB:** {db.db_path}")
|
|
149
|
+
st.sidebar.write(f"**News articles:** {stats['total_news_articles']}")
|
|
150
|
+
st.sidebar.write(f"**Market snapshots:** {stats['total_market_snapshots']}")
|
package/gossip/db.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite database for all Open Trademaxxxing state.
|
|
3
|
+
|
|
4
|
+
Stores: trades, news articles, market snapshots, agent cycle logs.
|
|
5
|
+
Single file at data/gossip.db — zero config, works with Streamlit.
|
|
6
|
+
|
|
7
|
+
Usage from other modules:
|
|
8
|
+
from gossip.db import GossipDB
|
|
9
|
+
db = GossipDB()
|
|
10
|
+
db.insert_trade(...)
|
|
11
|
+
db.get_open_positions()
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sqlite3
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
|
|
23
|
+
DB_PATH = DATA_DIR / "gossip.db"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GossipDB:
|
|
27
|
+
def __init__(self, db_path: Path | str | None = None):
|
|
28
|
+
self.db_path = Path(db_path) if db_path else DB_PATH
|
|
29
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
self.conn = sqlite3.connect(str(self.db_path))
|
|
31
|
+
self.conn.row_factory = sqlite3.Row
|
|
32
|
+
self.conn.execute("PRAGMA journal_mode=WAL")
|
|
33
|
+
self.conn.execute("PRAGMA foreign_keys=ON")
|
|
34
|
+
self._create_tables()
|
|
35
|
+
|
|
36
|
+
def _create_tables(self):
|
|
37
|
+
self.conn.executescript("""
|
|
38
|
+
CREATE TABLE IF NOT EXISTS trades (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
timestamp TEXT NOT NULL,
|
|
41
|
+
ticker TEXT NOT NULL,
|
|
42
|
+
title TEXT,
|
|
43
|
+
category TEXT,
|
|
44
|
+
side TEXT NOT NULL,
|
|
45
|
+
action TEXT NOT NULL DEFAULT 'buy',
|
|
46
|
+
contracts INTEGER NOT NULL,
|
|
47
|
+
entry_price REAL NOT NULL,
|
|
48
|
+
cost REAL NOT NULL,
|
|
49
|
+
fee REAL NOT NULL DEFAULT 0,
|
|
50
|
+
estimated_prob REAL,
|
|
51
|
+
edge REAL,
|
|
52
|
+
confidence TEXT,
|
|
53
|
+
reasoning TEXT,
|
|
54
|
+
news_trigger TEXT,
|
|
55
|
+
sources TEXT,
|
|
56
|
+
settled INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
outcome TEXT DEFAULT '',
|
|
58
|
+
pnl REAL DEFAULT 0,
|
|
59
|
+
exit_price REAL,
|
|
60
|
+
exit_reasoning TEXT,
|
|
61
|
+
exit_timestamp TEXT
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS portfolio (
|
|
65
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
66
|
+
bankroll REAL NOT NULL DEFAULT 30.0,
|
|
67
|
+
total_pnl REAL NOT NULL DEFAULT 0.0,
|
|
68
|
+
total_trades INTEGER NOT NULL DEFAULT 0,
|
|
69
|
+
wins INTEGER NOT NULL DEFAULT 0,
|
|
70
|
+
losses INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
updated_at TEXT
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
CREATE TABLE IF NOT EXISTS news (
|
|
75
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
|
+
timestamp TEXT NOT NULL,
|
|
77
|
+
source TEXT,
|
|
78
|
+
keyword TEXT,
|
|
79
|
+
title TEXT,
|
|
80
|
+
url TEXT,
|
|
81
|
+
snippet TEXT,
|
|
82
|
+
full_text TEXT,
|
|
83
|
+
relevance_score REAL,
|
|
84
|
+
matched_ticker TEXT,
|
|
85
|
+
processed INTEGER NOT NULL DEFAULT 0
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
CREATE TABLE IF NOT EXISTS market_snapshots (
|
|
89
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
90
|
+
timestamp TEXT NOT NULL,
|
|
91
|
+
ticker TEXT NOT NULL,
|
|
92
|
+
title TEXT,
|
|
93
|
+
category TEXT,
|
|
94
|
+
yes_bid REAL,
|
|
95
|
+
yes_ask REAL,
|
|
96
|
+
mid REAL,
|
|
97
|
+
volume REAL,
|
|
98
|
+
open_interest REAL,
|
|
99
|
+
close_time TEXT
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS agent_logs (
|
|
103
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
|
+
timestamp TEXT NOT NULL,
|
|
105
|
+
cycle_number INTEGER,
|
|
106
|
+
session_id TEXT,
|
|
107
|
+
duration_s REAL,
|
|
108
|
+
status TEXT,
|
|
109
|
+
markets_scanned INTEGER DEFAULT 0,
|
|
110
|
+
news_scraped INTEGER DEFAULT 0,
|
|
111
|
+
trades_made INTEGER DEFAULT 0,
|
|
112
|
+
output_summary TEXT
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_trades_ticker ON trades(ticker);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_trades_settled ON trades(settled);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_news_timestamp ON news(timestamp);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_news_keyword ON news(keyword);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_ticker ON market_snapshots(ticker);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_timestamp ON market_snapshots(timestamp);
|
|
121
|
+
""")
|
|
122
|
+
|
|
123
|
+
# Ensure portfolio row exists
|
|
124
|
+
row = self.conn.execute("SELECT COUNT(*) FROM portfolio").fetchone()
|
|
125
|
+
if row[0] == 0:
|
|
126
|
+
bankroll = float(os.getenv("BANKROLL", "15.0"))
|
|
127
|
+
self.conn.execute(
|
|
128
|
+
"INSERT INTO portfolio (id, bankroll, updated_at) VALUES (1, ?, ?)",
|
|
129
|
+
(bankroll, _now()),
|
|
130
|
+
)
|
|
131
|
+
self.conn.commit()
|
|
132
|
+
|
|
133
|
+
# --- Portfolio ---
|
|
134
|
+
|
|
135
|
+
def get_portfolio(self) -> dict:
|
|
136
|
+
row = self.conn.execute("SELECT * FROM portfolio WHERE id=1").fetchone()
|
|
137
|
+
return dict(row)
|
|
138
|
+
|
|
139
|
+
def update_portfolio(self, **kwargs) -> None:
|
|
140
|
+
kwargs["updated_at"] = _now()
|
|
141
|
+
sets = ", ".join(f"{k}=?" for k in kwargs)
|
|
142
|
+
vals = list(kwargs.values())
|
|
143
|
+
self.conn.execute(f"UPDATE portfolio SET {sets} WHERE id=1", vals)
|
|
144
|
+
self.conn.commit()
|
|
145
|
+
|
|
146
|
+
# --- Trades ---
|
|
147
|
+
|
|
148
|
+
def insert_trade(self, **kwargs) -> int:
|
|
149
|
+
if "sources" in kwargs and isinstance(kwargs["sources"], list):
|
|
150
|
+
kwargs["sources"] = json.dumps(kwargs["sources"])
|
|
151
|
+
kwargs.setdefault("timestamp", _now())
|
|
152
|
+
|
|
153
|
+
cols = ", ".join(kwargs.keys())
|
|
154
|
+
placeholders = ", ".join("?" * len(kwargs))
|
|
155
|
+
cur = self.conn.execute(
|
|
156
|
+
f"INSERT INTO trades ({cols}) VALUES ({placeholders})",
|
|
157
|
+
list(kwargs.values()),
|
|
158
|
+
)
|
|
159
|
+
self.conn.commit()
|
|
160
|
+
|
|
161
|
+
# Update portfolio
|
|
162
|
+
p = self.get_portfolio()
|
|
163
|
+
cost = kwargs.get("cost", 0) + kwargs.get("fee", 0)
|
|
164
|
+
self.update_portfolio(
|
|
165
|
+
bankroll=round(p["bankroll"] - cost, 2),
|
|
166
|
+
total_trades=p["total_trades"] + 1,
|
|
167
|
+
)
|
|
168
|
+
return cur.lastrowid
|
|
169
|
+
|
|
170
|
+
def get_open_positions(self) -> list[dict]:
|
|
171
|
+
rows = self.conn.execute(
|
|
172
|
+
"SELECT * FROM trades WHERE settled=0 AND action='buy' ORDER BY timestamp DESC"
|
|
173
|
+
).fetchall()
|
|
174
|
+
return [dict(r) for r in rows]
|
|
175
|
+
|
|
176
|
+
def get_trade_history(self, limit: int = 50) -> list[dict]:
|
|
177
|
+
rows = self.conn.execute(
|
|
178
|
+
"SELECT * FROM trades ORDER BY timestamp DESC LIMIT ?", (limit,)
|
|
179
|
+
).fetchall()
|
|
180
|
+
return [dict(r) for r in rows]
|
|
181
|
+
|
|
182
|
+
def settle_trade(self, ticker: str, outcome_yes: bool) -> dict | None:
|
|
183
|
+
row = self.conn.execute(
|
|
184
|
+
"SELECT * FROM trades WHERE ticker=? AND settled=0 ORDER BY timestamp DESC LIMIT 1",
|
|
185
|
+
(ticker,),
|
|
186
|
+
).fetchone()
|
|
187
|
+
if not row:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
trade = dict(row)
|
|
191
|
+
won = (trade["side"] == "yes" and outcome_yes) or (trade["side"] == "no" and not outcome_yes)
|
|
192
|
+
if won:
|
|
193
|
+
pnl = round((1.0 - trade["entry_price"]) * trade["contracts"], 2)
|
|
194
|
+
outcome = "win"
|
|
195
|
+
else:
|
|
196
|
+
pnl = round(-trade["entry_price"] * trade["contracts"], 2)
|
|
197
|
+
outcome = "loss"
|
|
198
|
+
|
|
199
|
+
self.conn.execute(
|
|
200
|
+
"UPDATE trades SET settled=1, outcome=?, pnl=?, exit_timestamp=? WHERE id=?",
|
|
201
|
+
(outcome, pnl, _now(), trade["id"]),
|
|
202
|
+
)
|
|
203
|
+
self.conn.commit()
|
|
204
|
+
|
|
205
|
+
p = self.get_portfolio()
|
|
206
|
+
bankroll = round(p["bankroll"] + trade["entry_price"] * trade["contracts"] + pnl, 2)
|
|
207
|
+
wins = p["wins"] + (1 if won else 0)
|
|
208
|
+
losses = p["losses"] + (0 if won else 1)
|
|
209
|
+
self.update_portfolio(bankroll=bankroll, total_pnl=round(p["total_pnl"] + pnl, 2), wins=wins, losses=losses)
|
|
210
|
+
|
|
211
|
+
return {"ticker": ticker, "outcome": outcome, "pnl": pnl, "bankroll": bankroll}
|
|
212
|
+
|
|
213
|
+
def exit_trade(self, ticker: str, exit_price: float, reasoning: str = "") -> dict | None:
|
|
214
|
+
row = self.conn.execute(
|
|
215
|
+
"SELECT * FROM trades WHERE ticker=? AND settled=0 ORDER BY timestamp DESC LIMIT 1",
|
|
216
|
+
(ticker,),
|
|
217
|
+
).fetchone()
|
|
218
|
+
if not row:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
trade = dict(row)
|
|
222
|
+
pnl = round((exit_price - trade["entry_price"]) * trade["contracts"], 2)
|
|
223
|
+
outcome = "win" if pnl > 0 else "loss"
|
|
224
|
+
|
|
225
|
+
self.conn.execute(
|
|
226
|
+
"UPDATE trades SET settled=1, outcome=?, pnl=?, exit_price=?, exit_reasoning=?, exit_timestamp=? WHERE id=?",
|
|
227
|
+
(outcome, pnl, exit_price, reasoning, _now(), trade["id"]),
|
|
228
|
+
)
|
|
229
|
+
self.conn.commit()
|
|
230
|
+
|
|
231
|
+
p = self.get_portfolio()
|
|
232
|
+
proceeds = exit_price * trade["contracts"]
|
|
233
|
+
bankroll = round(p["bankroll"] + proceeds, 2)
|
|
234
|
+
wins = p["wins"] + (1 if pnl > 0 else 0)
|
|
235
|
+
losses = p["losses"] + (0 if pnl > 0 else 1)
|
|
236
|
+
self.update_portfolio(bankroll=bankroll, total_pnl=round(p["total_pnl"] + pnl, 2), wins=wins, losses=losses)
|
|
237
|
+
|
|
238
|
+
return {"ticker": ticker, "outcome": outcome, "pnl": pnl, "exit_price": exit_price, "bankroll": bankroll}
|
|
239
|
+
|
|
240
|
+
# --- News ---
|
|
241
|
+
|
|
242
|
+
def insert_news(self, articles: list[dict]) -> int:
|
|
243
|
+
ts = _now()
|
|
244
|
+
count = 0
|
|
245
|
+
for a in articles:
|
|
246
|
+
self.conn.execute(
|
|
247
|
+
"INSERT INTO news (timestamp, source, keyword, title, url, snippet, full_text) VALUES (?,?,?,?,?,?,?)",
|
|
248
|
+
(ts, a.get("source", ""), a.get("keyword", ""), a.get("title", ""),
|
|
249
|
+
a.get("url", ""), a.get("snippet", ""), a.get("text", "")),
|
|
250
|
+
)
|
|
251
|
+
count += 1
|
|
252
|
+
self.conn.commit()
|
|
253
|
+
return count
|
|
254
|
+
|
|
255
|
+
def get_recent_news(self, hours: int = 24, limit: int = 100) -> list[dict]:
|
|
256
|
+
rows = self.conn.execute(
|
|
257
|
+
"SELECT * FROM news ORDER BY timestamp DESC LIMIT ?", (limit,)
|
|
258
|
+
).fetchall()
|
|
259
|
+
return [dict(r) for r in rows]
|
|
260
|
+
|
|
261
|
+
def get_news_for_ticker(self, ticker: str, limit: int = 20) -> list[dict]:
|
|
262
|
+
rows = self.conn.execute(
|
|
263
|
+
"SELECT * FROM news WHERE matched_ticker=? ORDER BY timestamp DESC LIMIT ?",
|
|
264
|
+
(ticker, limit),
|
|
265
|
+
).fetchall()
|
|
266
|
+
return [dict(r) for r in rows]
|
|
267
|
+
|
|
268
|
+
# --- Market snapshots ---
|
|
269
|
+
|
|
270
|
+
def insert_market_snapshot(self, market: dict) -> None:
|
|
271
|
+
self.conn.execute(
|
|
272
|
+
"""INSERT INTO market_snapshots
|
|
273
|
+
(timestamp, ticker, title, category, yes_bid, yes_ask, mid, volume, open_interest, close_time)
|
|
274
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)""",
|
|
275
|
+
(_now(), market.get("ticker", ""), market.get("title", ""),
|
|
276
|
+
market.get("category", ""), market.get("yes_bid", 0),
|
|
277
|
+
market.get("yes_ask", 0), market.get("mid", 0),
|
|
278
|
+
market.get("volume", 0), market.get("open_interest", 0),
|
|
279
|
+
market.get("close_time", "")),
|
|
280
|
+
)
|
|
281
|
+
self.conn.commit()
|
|
282
|
+
|
|
283
|
+
def insert_market_snapshots(self, markets: list[dict]) -> int:
|
|
284
|
+
ts = _now()
|
|
285
|
+
for m in markets:
|
|
286
|
+
self.conn.execute(
|
|
287
|
+
"""INSERT INTO market_snapshots
|
|
288
|
+
(timestamp, ticker, title, category, yes_bid, yes_ask, mid, volume, open_interest, close_time)
|
|
289
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)""",
|
|
290
|
+
(ts, m.get("ticker", ""), m.get("title", ""),
|
|
291
|
+
m.get("category", ""), m.get("yes_bid", 0),
|
|
292
|
+
m.get("yes_ask", 0), m.get("mid", 0),
|
|
293
|
+
m.get("volume", 0), m.get("open_interest", 0),
|
|
294
|
+
m.get("close_time", "")),
|
|
295
|
+
)
|
|
296
|
+
self.conn.commit()
|
|
297
|
+
return len(markets)
|
|
298
|
+
|
|
299
|
+
def get_market_history(self, ticker: str, limit: int = 100) -> list[dict]:
|
|
300
|
+
rows = self.conn.execute(
|
|
301
|
+
"SELECT * FROM market_snapshots WHERE ticker=? ORDER BY timestamp DESC LIMIT ?",
|
|
302
|
+
(ticker, limit),
|
|
303
|
+
).fetchall()
|
|
304
|
+
return [dict(r) for r in rows]
|
|
305
|
+
|
|
306
|
+
def get_latest_snapshots(self, limit: int = 50) -> list[dict]:
|
|
307
|
+
rows = self.conn.execute(
|
|
308
|
+
"""SELECT ms.* FROM market_snapshots ms
|
|
309
|
+
INNER JOIN (SELECT ticker, MAX(timestamp) as max_ts FROM market_snapshots GROUP BY ticker) latest
|
|
310
|
+
ON ms.ticker = latest.ticker AND ms.timestamp = latest.max_ts
|
|
311
|
+
ORDER BY ms.volume DESC LIMIT ?""",
|
|
312
|
+
(limit,),
|
|
313
|
+
).fetchall()
|
|
314
|
+
return [dict(r) for r in rows]
|
|
315
|
+
|
|
316
|
+
# --- Agent logs ---
|
|
317
|
+
|
|
318
|
+
def log_cycle(self, **kwargs) -> int:
|
|
319
|
+
kwargs.setdefault("timestamp", _now())
|
|
320
|
+
cols = ", ".join(kwargs.keys())
|
|
321
|
+
placeholders = ", ".join("?" * len(kwargs))
|
|
322
|
+
cur = self.conn.execute(
|
|
323
|
+
f"INSERT INTO agent_logs ({cols}) VALUES ({placeholders})",
|
|
324
|
+
list(kwargs.values()),
|
|
325
|
+
)
|
|
326
|
+
self.conn.commit()
|
|
327
|
+
return cur.lastrowid
|
|
328
|
+
|
|
329
|
+
def get_recent_cycles(self, limit: int = 20) -> list[dict]:
|
|
330
|
+
rows = self.conn.execute(
|
|
331
|
+
"SELECT * FROM agent_logs ORDER BY timestamp DESC LIMIT ?", (limit,)
|
|
332
|
+
).fetchall()
|
|
333
|
+
return [dict(r) for r in rows]
|
|
334
|
+
|
|
335
|
+
# --- Stats ---
|
|
336
|
+
|
|
337
|
+
def get_stats(self) -> dict:
|
|
338
|
+
p = self.get_portfolio()
|
|
339
|
+
open_count = self.conn.execute("SELECT COUNT(*) FROM trades WHERE settled=0 AND action='buy'").fetchone()[0]
|
|
340
|
+
total_news = self.conn.execute("SELECT COUNT(*) FROM news").fetchone()[0]
|
|
341
|
+
total_snapshots = self.conn.execute("SELECT COUNT(*) FROM market_snapshots").fetchone()[0]
|
|
342
|
+
total_cycles = self.conn.execute("SELECT COUNT(*) FROM agent_logs").fetchone()[0]
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
**p,
|
|
346
|
+
"open_positions": open_count,
|
|
347
|
+
"total_news_articles": total_news,
|
|
348
|
+
"total_market_snapshots": total_snapshots,
|
|
349
|
+
"total_agent_cycles": total_cycles,
|
|
350
|
+
"win_rate": round(p["wins"] / p["total_trades"], 3) if p["total_trades"] > 0 else 0,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
def close(self):
|
|
354
|
+
self.conn.close()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _now() -> str:
|
|
358
|
+
return datetime.now(timezone.utc).isoformat()
|