Skip to content

Commit 665a750

Browse files
Merge pull request #80 from spiceoogway/feat/me-positions-and-bets
feat: add GET /me/positions and GET /me/bets endpoints
2 parents 2fbb11d + 72d93e4 commit 665a750

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

api.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
AgentReputationResponse,
4343
TradingScoreResponse, ResolutionScoreResponse,
4444
CreationScoreResponse, ParticipationScoreResponse,
45+
PortfolioPosition, PortfolioSummary, PortfolioResponse, UserBetHistoryItem,
4546
)
4647
from resolver import resolve_market, get_resolution_summary
4748
from reputation import compute_reputation
@@ -1284,6 +1285,27 @@ def update_position(self, market_id: str, user_id: str,
12841285
finally:
12851286
self._put_conn(conn)
12861287

1288+
def get_user_positions(self, user_id: str) -> List[dict]:
1289+
"""Get all positions for a user across all markets."""
1290+
if self._use_memory:
1291+
positions = []
1292+
for market_id, market_positions in self._positions.items():
1293+
if user_id in market_positions:
1294+
positions.append(market_positions[user_id])
1295+
return positions
1296+
1297+
conn = self._get_conn()
1298+
try:
1299+
with conn.cursor() as cur:
1300+
cur.execute(
1301+
"SELECT * FROM positions WHERE user_id = %s",
1302+
(user_id,),
1303+
)
1304+
rows = cur.fetchall()
1305+
return [self._row_to_position(row) for row in rows]
1306+
finally:
1307+
self._put_conn(conn)
1308+
12871309
def reduce_position(self, market_id: str, user_id: str,
12881310
outcome: Outcome, shares: float):
12891311
"""Reduce shares in a position (for selling)."""
@@ -2420,6 +2442,105 @@ async def get_me(user: dict = Depends(require_auth)):
24202442
)
24212443

24222444

2445+
@app.get("/me/positions", response_model=PortfolioResponse)
2446+
async def get_my_positions(user: dict = Depends(require_auth)):
2447+
"""Get all positions for the authenticated agent across all markets.
2448+
2449+
Returns a portfolio overview with per-market positions and an aggregate summary.
2450+
Saves agents from making N+1 calls to /markets/{id}/positions.
2451+
"""
2452+
positions = db.get_user_positions(user["id"])
2453+
items: List[PortfolioPosition] = []
2454+
total_invested = 0.0
2455+
total_current_value = 0.0
2456+
open_count = 0
2457+
resolved_count = 0
2458+
2459+
for pos in positions:
2460+
market = db.get_market(pos["market_id"])
2461+
if not market:
2462+
continue
2463+
2464+
prob = get_cpmm_probability(market["pool"], market["p"])
2465+
current_value = pos["yes_shares"] * prob + pos["no_shares"] * (1 - prob)
2466+
pnl = current_value - pos["total_invested"]
2467+
2468+
is_open = market["status"] in (MarketStatus.OPEN, MarketStatus.RESOLVING)
2469+
if is_open:
2470+
open_count += 1
2471+
else:
2472+
resolved_count += 1
2473+
2474+
total_invested += pos["total_invested"]
2475+
total_current_value += current_value
2476+
2477+
items.append(PortfolioPosition(
2478+
market_id=pos["market_id"],
2479+
market_title=market["title"],
2480+
market_status=market["status"],
2481+
yes_shares=pos["yes_shares"],
2482+
no_shares=pos["no_shares"],
2483+
total_invested=pos["total_invested"],
2484+
current_value=round(current_value, 4),
2485+
pnl=round(pnl, 4),
2486+
current_probability=round(prob, 6),
2487+
))
2488+
2489+
return PortfolioResponse(
2490+
positions=items,
2491+
summary=PortfolioSummary(
2492+
total_invested=round(total_invested, 4),
2493+
total_current_value=round(total_current_value, 4),
2494+
total_pnl=round(total_current_value - total_invested, 4),
2495+
open_positions=open_count,
2496+
resolved_positions=resolved_count,
2497+
),
2498+
)
2499+
2500+
2501+
@app.get("/me/bets", response_model=List[UserBetHistoryItem])
2502+
async def get_my_bets(
2503+
limit: int = 50,
2504+
offset: int = 0,
2505+
user: dict = Depends(require_auth),
2506+
):
2507+
"""Get the authenticated agent's trade history across all markets.
2508+
2509+
Supports basic pagination via limit/offset.
2510+
Returns bets sorted by created_at DESC (newest first).
2511+
"""
2512+
if limit < 1:
2513+
limit = 1
2514+
if limit > 200:
2515+
limit = 200
2516+
if offset < 0:
2517+
offset = 0
2518+
2519+
all_bets = db.get_bets_for_user(user["id"])
2520+
# Sort newest first
2521+
all_bets.sort(key=lambda b: b["created_at"], reverse=True)
2522+
page = all_bets[offset : offset + limit]
2523+
2524+
items: List[UserBetHistoryItem] = []
2525+
for bet in page:
2526+
market = db.get_market(bet["market_id"])
2527+
market_title = market["title"] if market else "Unknown market"
2528+
items.append(UserBetHistoryItem(
2529+
bet_id=bet["id"],
2530+
market_id=bet["market_id"],
2531+
market_title=market_title,
2532+
outcome=bet["outcome"],
2533+
amount=bet["amount"],
2534+
shares=bet["shares"],
2535+
avg_price=bet["avg_price"],
2536+
probability_before=bet["probability_before"],
2537+
probability_after=bet["probability_after"],
2538+
created_at=bet["created_at"],
2539+
))
2540+
2541+
return items
2542+
2543+
24232544
@app.get("/users/{user_id}", response_model=UserProfile)
24242545
async def get_user(user_id: str):
24252546
"""Get public user profile."""

models.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,51 @@ class UserMe(BaseModel):
228228
profit_all_time: float
229229

230230

231+
class PortfolioPosition(BaseModel):
232+
"""A single position in the agent's portfolio (cross-market view)."""
233+
market_id: str
234+
market_title: str
235+
market_status: MarketStatus
236+
yes_shares: float
237+
no_shares: float
238+
total_invested: float
239+
current_value: float
240+
pnl: float
241+
current_probability: float
242+
currency: str = "ŧ"
243+
244+
245+
class PortfolioSummary(BaseModel):
246+
"""Summary statistics for an agent's entire portfolio."""
247+
total_invested: float
248+
total_current_value: float
249+
total_pnl: float
250+
open_positions: int
251+
resolved_positions: int
252+
currency: str = "ŧ"
253+
254+
255+
class PortfolioResponse(BaseModel):
256+
"""Full portfolio response for GET /me/positions."""
257+
positions: List[PortfolioPosition]
258+
summary: PortfolioSummary
259+
260+
261+
class UserBetHistoryItem(BaseModel):
262+
"""A single bet in the agent's trade history (cross-market view)."""
263+
bet_id: str
264+
market_id: str
265+
market_title: str
266+
outcome: Outcome
267+
amount: float
268+
shares: float
269+
avg_price: float
270+
probability_before: float
271+
probability_after: float
272+
created_at: datetime
273+
currency: str = "ŧ"
274+
275+
231276
class LeaderboardEntry(BaseModel):
232277
"""Entry in the leaderboard. All amounts are in points (ŧ)."""
233278
user_id: str

0 commit comments

Comments
 (0)