|
42 | 42 | AgentReputationResponse, |
43 | 43 | TradingScoreResponse, ResolutionScoreResponse, |
44 | 44 | CreationScoreResponse, ParticipationScoreResponse, |
| 45 | + PortfolioPosition, PortfolioSummary, PortfolioResponse, UserBetHistoryItem, |
45 | 46 | ) |
46 | 47 | from resolver import resolve_market, get_resolution_summary |
47 | 48 | from reputation import compute_reputation |
@@ -1284,6 +1285,27 @@ def update_position(self, market_id: str, user_id: str, |
1284 | 1285 | finally: |
1285 | 1286 | self._put_conn(conn) |
1286 | 1287 |
|
| 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 | + |
1287 | 1309 | def reduce_position(self, market_id: str, user_id: str, |
1288 | 1310 | outcome: Outcome, shares: float): |
1289 | 1311 | """Reduce shares in a position (for selling).""" |
@@ -2420,6 +2442,105 @@ async def get_me(user: dict = Depends(require_auth)): |
2420 | 2442 | ) |
2421 | 2443 |
|
2422 | 2444 |
|
| 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 | + |
2423 | 2544 | @app.get("/users/{user_id}", response_model=UserProfile) |
2424 | 2545 | async def get_user(user_id: str): |
2425 | 2546 | """Get public user profile.""" |
|
0 commit comments