Skip to content

Commit 3ed8285

Browse files
authored
Merge pull request #179 from shirtlessfounder/feat/platform-stats-177
feat: add /stats endpoint for live platform statistics
2 parents 4e3d109 + 66c1371 commit 3ed8285

4 files changed

Lines changed: 134 additions & 0 deletions

File tree

routes/meta.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,29 @@ async def get_currency():
132132
"starting_balance": STARTING_BALANCE,
133133
"note": "MoltMarkets uses points (ŧ), not real money. All balances and amounts are in points.",
134134
}
135+
136+
137+
@router.get("/stats")
138+
async def get_platform_stats():
139+
"""Get live platform statistics for homepage display.
140+
141+
Returns counts for agents (with claimed/pending breakdown),
142+
markets, and total trading volume.
143+
144+
See: https://github.com/shirtlessfounder/moltmarkets-api/issues/177
145+
"""
146+
db = get_db()
147+
agent_counts = db.count_agents_by_status()
148+
market_count = db.count_markets()
149+
total_volume = db.get_platform_volume()
150+
151+
return {
152+
"agents": {
153+
"total": agent_counts["total"],
154+
"claimed": agent_counts["claimed"],
155+
"pending": agent_counts["pending"],
156+
},
157+
"markets": market_count,
158+
"volume": round(total_volume, 3),
159+
"currency": CURRENCY_SYMBOL,
160+
}

storage/markets.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,24 @@ def count_markets(self) -> int:
9494
finally:
9595
self._put_conn(conn)
9696

97+
def get_platform_volume(self) -> float:
98+
"""Get total trading volume across all markets.
99+
100+
O(1) via SUM aggregate instead of O(N) full-table load.
101+
102+
See: https://github.com/shirtlessfounder/moltmarkets-api/issues/177
103+
"""
104+
if self._use_memory:
105+
return sum(m.get("total_volume", 0.0) for m in self._markets.values())
106+
107+
conn = self._get_conn()
108+
try:
109+
with conn.cursor() as cur:
110+
cur.execute("SELECT COALESCE(SUM(total_volume), 0) AS total FROM markets")
111+
return float(cur.fetchone()["total"])
112+
finally:
113+
self._put_conn(conn)
114+
97115
def get_markets_by_ids(self, market_ids: set) -> Dict[str, MarketDict]:
98116
"""Get specific markets by their IDs in a single query.
99117

storage/users.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,55 @@ def count_users(self) -> int:
351351
finally:
352352
self._put_conn(conn)
353353

354+
def count_agents_by_status(self) -> dict:
355+
"""Count agents grouped by status (claimed/pending/etc).
356+
357+
Returns dict with keys: total, claimed, pending, other.
358+
Excludes sandbox agents from counts.
359+
360+
See: https://github.com/shirtlessfounder/moltmarkets-api/issues/177
361+
"""
362+
if self._use_memory:
363+
total = claimed = pending = 0
364+
for user in self._users.values():
365+
if user.get("user_type") != "agent" or user.get("is_sandbox"):
366+
continue
367+
total += 1
368+
status = user.get("status", "pending")
369+
if status == "claimed":
370+
claimed += 1
371+
elif status == "pending":
372+
pending += 1
373+
return {
374+
"total": total,
375+
"claimed": claimed,
376+
"pending": pending,
377+
"other": total - claimed - pending,
378+
}
379+
380+
conn = self._get_conn()
381+
try:
382+
with conn.cursor() as cur:
383+
cur.execute("""
384+
SELECT
385+
COUNT(*) FILTER (WHERE user_type = 'agent' AND NOT is_sandbox) AS total,
386+
COUNT(*) FILTER (WHERE user_type = 'agent' AND NOT is_sandbox AND status = 'claimed') AS claimed,
387+
COUNT(*) FILTER (WHERE user_type = 'agent' AND NOT is_sandbox AND status = 'pending') AS pending
388+
FROM users
389+
""")
390+
row = cur.fetchone()
391+
total = row["total"] or 0
392+
claimed = row["claimed"] or 0
393+
pending = row["pending"] or 0
394+
return {
395+
"total": total,
396+
"claimed": claimed,
397+
"pending": pending,
398+
"other": total - claimed - pending,
399+
}
400+
finally:
401+
self._put_conn(conn)
402+
354403
def reset_sandbox_balance(self, user_id: str, amount: float) -> float:
355404
"""Reset a sandbox agent's balance and stats.
356405

test_stats.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Tests for /stats endpoint."""
2+
3+
from fastapi.testclient import TestClient
4+
5+
from api import app
6+
7+
client = TestClient(app)
8+
9+
10+
def test_stats_endpoint_returns_counts():
11+
"""Test /stats returns agent counts, market count, and volume."""
12+
response = client.get("/stats")
13+
assert response.status_code == 200
14+
data = response.json()
15+
16+
# Check structure
17+
assert "agents" in data
18+
assert "markets" in data
19+
assert "volume" in data
20+
assert "currency" in data
21+
22+
# Check agents breakdown
23+
agents = data["agents"]
24+
assert "total" in agents
25+
assert "claimed" in agents
26+
assert "pending" in agents
27+
assert agents["total"] >= 0
28+
assert agents["claimed"] >= 0
29+
assert agents["pending"] >= 0
30+
assert agents["total"] >= agents["claimed"] + agents["pending"]
31+
32+
# Check types
33+
assert isinstance(data["markets"], int)
34+
assert isinstance(data["volume"], (int, float))
35+
assert data["currency"] == "ŧ"
36+
37+
38+
def test_stats_endpoint_no_auth_required():
39+
"""Test /stats is publicly accessible without authentication."""
40+
response = client.get("/stats")
41+
assert response.status_code == 200

0 commit comments

Comments
 (0)