KOL feeds: fix dead/blocked sources, drop stale feeds (29→25)
Feed-health pass over KOL_FEEDS: - raoulpal: stale Substack (last 2024-05) → Real Vision podcast feed - dampedspring: paywalled (0 entries) → free "Damped Spring 101" Substack - unchained: Cloudflare 403 → canonical Megaphone podcast feed - lynalden: Cloudflare 202 → FeedBurner mirror - glassnode: recovered via httpx http2=True (was 403 on HTTP/1.1) - browser User-Agent + Accept headers on feed fetch - removed dead feeds with no active replacement: placeholder, dragonfly, niccarter, eugene - pin h2==4.3.0 (required by http2=True) All 25 remaining feeds verified fetching real body content; newest post per feed ≤88d. Bundles in-flight KOL-module work already in the working tree (kol_x ingest, migration 027, tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+41
-17
@@ -199,11 +199,15 @@ async def get_today_stats(
|
||||
hour=0, minute=0, second=0, microsecond=0, tzinfo=None
|
||||
)
|
||||
|
||||
# Exclude released trades — these were released back to user control and
|
||||
# closed by the user on HL directly, not by the bot. Including them would
|
||||
# inflate the bot's "today" P&L with trades the bot didn't manage.
|
||||
closed_rows = await db.execute(
|
||||
select(BotTrade).where(
|
||||
BotTrade.wallet_address == wallet,
|
||||
BotTrade.closed_at >= midnight,
|
||||
BotTrade.pnl_usd.is_not(None),
|
||||
BotTrade.released_at.is_(None),
|
||||
)
|
||||
)
|
||||
closed = closed_rows.scalars().all()
|
||||
@@ -280,6 +284,15 @@ async def manual_close(
|
||||
if not wallet or not isinstance(timestamp, int) or not isinstance(signature, str):
|
||||
raise HTTPException(422, "wallet, timestamp, signature required")
|
||||
|
||||
# Verify signature first — prevents trade_id enumeration via 403/409 before auth.
|
||||
verify_signed_request(
|
||||
action=ACTION_CLOSE_TRADE,
|
||||
wallet=wallet,
|
||||
timestamp_ms=timestamp,
|
||||
signature=signature,
|
||||
body={"trade_id": trade_id},
|
||||
)
|
||||
|
||||
# Load the trade. Must be open AND owned by the signing wallet.
|
||||
trade = (await db.execute(select(BotTrade).where(BotTrade.id == trade_id))).scalar_one_or_none()
|
||||
if trade is None:
|
||||
@@ -289,14 +302,6 @@ async def manual_close(
|
||||
if trade.closed_at is not None:
|
||||
raise HTTPException(409, f"trade {trade_id} is already closed")
|
||||
|
||||
verify_signed_request(
|
||||
action=ACTION_CLOSE_TRADE,
|
||||
wallet=wallet,
|
||||
timestamp_ms=timestamp,
|
||||
signature=signature,
|
||||
body={"trade_id": trade_id},
|
||||
)
|
||||
|
||||
# Find the API key from the subscription.
|
||||
sub = (await db.execute(
|
||||
select(Subscription).where(Subscription.wallet_address == wallet)
|
||||
@@ -330,7 +335,26 @@ async def manual_close(
|
||||
force=True,
|
||||
)
|
||||
|
||||
closed = (await db.execute(select(BotTrade).where(BotTrade.id == trade_id))).scalar_one()
|
||||
# B45: close_and_finalize uses its own AsyncSessionLocal session, so the
|
||||
# route's `db` session has a stale identity-map cache for this trade row.
|
||||
# `populate_existing=True` forces SQLAlchemy to overwrite the cached
|
||||
# instance with the freshly-committed values (exit_price, pnl_usd, etc.)
|
||||
# rather than returning the pre-close snapshot from the identity map.
|
||||
closed = (await db.execute(
|
||||
select(BotTrade).where(BotTrade.id == trade_id).execution_options(populate_existing=True)
|
||||
)).scalar_one()
|
||||
|
||||
# B46: close_and_finalize returns silently on certain failures (no price
|
||||
# for paper close, HL returns no fill, etc.) without raising an exception.
|
||||
# Detect the failure by checking whether closed_at was actually written.
|
||||
if closed.closed_at is None:
|
||||
raise HTTPException(
|
||||
500,
|
||||
"Close command issued but the position could not be closed "
|
||||
"(no price feed, HL fill failure, or another caller closed it first). "
|
||||
"Refresh open positions — if it still shows, retry or close manually on HL."
|
||||
)
|
||||
|
||||
return CloseTradeResponse(
|
||||
status="ok",
|
||||
trade_id=trade_id,
|
||||
@@ -373,14 +397,6 @@ async def set_trade_grow(
|
||||
if body_tid != trade_id:
|
||||
raise HTTPException(400, "trade_id mismatch (path vs signed body)")
|
||||
|
||||
trade = (await db.execute(select(BotTrade).where(BotTrade.id == trade_id))).scalar_one_or_none()
|
||||
if trade is None:
|
||||
raise HTTPException(404, f"trade {trade_id} not found")
|
||||
if trade.wallet_address.lower() != wallet:
|
||||
raise HTTPException(403, "trade belongs to a different wallet")
|
||||
if trade.closed_at is not None:
|
||||
raise HTTPException(409, f"trade {trade_id} is already closed")
|
||||
|
||||
verify_signed_request(
|
||||
action=ACTION_SET_GROW,
|
||||
wallet=wallet,
|
||||
@@ -389,6 +405,14 @@ async def set_trade_grow(
|
||||
body={"trade_id": trade_id, "enabled": enabled},
|
||||
)
|
||||
|
||||
trade = (await db.execute(select(BotTrade).where(BotTrade.id == trade_id))).scalar_one_or_none()
|
||||
if trade is None:
|
||||
raise HTTPException(404, f"trade {trade_id} not found")
|
||||
if trade.wallet_address.lower() != wallet:
|
||||
raise HTTPException(403, "trade belongs to a different wallet")
|
||||
if trade.closed_at is not None:
|
||||
raise HTTPException(409, f"trade {trade_id} is already closed")
|
||||
|
||||
trade.grow_mode = enabled
|
||||
await db.commit()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user