polymarket-browse: add --search, --telegram, --matches-only, --non-matches-only flags; fix partial fetch warnings; clean up output formatting
This commit is contained in:
@@ -12,73 +12,74 @@ Browse tradeable Polymarket prediction market events by game category.
|
||||
|
||||
**For Hermes Agent users:**
|
||||
```bash
|
||||
hermes skills install https://git.fbrns.co/shoko/jujutsu-skills#polymarket-browse
|
||||
hermes skills install https://github.com/shokollm/jujutsu-skills#polymarket-browse
|
||||
```
|
||||
|
||||
**For OpenClaw users:**
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://git.fbrns.co/shoko/jujutsu-skills.git ~/jujutsu-skills
|
||||
git clone https://github.com/shokollm/jujutsu-skills.git ~/jujutsu-skills
|
||||
|
||||
# Copy skill to your OpenClaw skills folder
|
||||
cp -r ~/jujutsu-skills/skills/polymarket-browse ~/.openclaw/skills/
|
||||
```
|
||||
|
||||
**Manual installation:**
|
||||
**Optional: For better experience, install the Polymarket MCP server:**
|
||||
This lets the agent answer questions about Polymarket markets, rules, and trading mechanics.
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://git.fbrns.co/shoko/jujutsu-skills.git ~/jujutsu-skills
|
||||
|
||||
# Copy skill to your Hermes skills folder
|
||||
cp -r ~/jujutsu-skills/skills/polymarket-browse ~/.hermes/skills/
|
||||
# Ask your agent to install it for you, or add to config.yaml:
|
||||
hermes mcp add polymarket https://docs.polymarket.com/mcp
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
polymarket-browse [--category "Counter Strike"] [--limit 5] [--detail N]
|
||||
polymarket-browse [--category "Counter Strike"] [--limit 5] [--matches N] [--non-matches N] [--search "TeamName"] [--matches-only] [--non-matches-only] [--detail N] [--raw] [--telegram]
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
- `--category` : Game category to browse. Options: All Esports, Counter Strike, League of Legends, Dota 2, Valorant, NBA, NFL, UFC, Tennis (default: Counter Strike)
|
||||
- `--limit` : Max number of events to show (default: 5)
|
||||
- `--detail` : Index of event (1-indexed) to show detailed markets for. Defaults to 1 (first event). Set to 0 to disable.
|
||||
- `--limit` : Max events per section (match + non-match). Default: 5
|
||||
- `--matches` : Max match markets to show. Default: --limit
|
||||
- `--non-matches` : Max non-match markets to show. Default: --limit
|
||||
- `--search` : Free-text team/term search within the selected category. Appends to the category query. Example: `--category "Counter Strike" --search "FlyQuest"`
|
||||
- `--matches-only` : Show only match markets (suppress non-match section).
|
||||
- `--non-matches-only` : Show only non-match markets (suppress match section).
|
||||
- `--detail` : Index of match event (1-indexed) to show detailed markets. Default: 1. Set to 0 to disable.
|
||||
- `--list-categories` : List available game categories and exit
|
||||
- `--raw` : Show all events without tradeable filter (for debugging). Includes fetch stats.
|
||||
- `--telegram` : Send results to Telegram. Requires `BOT_TOKEN` and `CHAT_ID` in environment variables.
|
||||
|
||||
## Output Format
|
||||
|
||||
Each event displays in a **6-line format**:
|
||||
Output is split into two sections:
|
||||
|
||||
```
|
||||
=== DOTA 2 ===
|
||||
Query: 'Dota2' | Total API: 54 | Tradeable: 3
|
||||
Current time (WIB): 13:58 WIB | Mar 24, 2026
|
||||
=== COUNTER STRIKE ===
|
||||
Current time (WIB): 17:00 WIB | Mar 24, 2026
|
||||
|
||||
1. [Dota 2: Yakult Brothers vs BetBoom Team (BO2)](https://polymarket.com/market/...)
|
||||
Mar 24, 19:00 WIB | In 5h
|
||||
MATCH MARKETS
|
||||
|
||||
Vol: $4,952
|
||||
Tournament: ESL One Birmingham Group A
|
||||
Odds: Yakult Brothers 29c | 71c BetBoom Team
|
||||
1. [Counter-Strike: TheMongolz vs Spirit (BO3)](https://polymarket.com/market/...)
|
||||
Mar 24, 03:45 WIB | LIVE
|
||||
Vol: $2,626,029
|
||||
Tournament: BLAST Open Rotterdam Group B
|
||||
Odds: TheMongolz 100c | 0c Spirit
|
||||
|
||||
2. [Dota 2: GamerLegion vs Team Yandex (BO2)](https://polymarket.com/market/...)
|
||||
Mar 24, 19:00 WIB | In 5h
|
||||
NON-MATCH MARKETS
|
||||
|
||||
Vol: $3,944
|
||||
Tournament: ESL One Birmingham Group A
|
||||
Odds: GamerLegion 28c | 72c Team Yandex
|
||||
1. [Blast Open Rotterdam 2026: Winner](https://polymarket.com/event/...)
|
||||
Feb 28, 04:43 WIB | 24d ago
|
||||
Markets: 17 | Total Vol: $958,116
|
||||
```
|
||||
|
||||
Format per event:
|
||||
- **Line 1**: `[number]. [Title](url)` — title without tournament name
|
||||
- **Line 2**: `Date, Time WIB | Relative` (e.g., "Mar 24, 19:00 WIB | In 5h")
|
||||
- **Line 3**: (empty separator)
|
||||
- **Line 4**: `Vol: $XXX`
|
||||
- **Line 5**: `Tournament: [tournament name]`
|
||||
- **Line 6**: `Odds: [Team A] [Odds A] | [Odds B] [Team B]`
|
||||
**Match Markets** are actual head-to-head matches with moneyline odds (sorted by volume).
|
||||
**Non-match Markets** are tournament futures, props, and other markets without direct match odds.
|
||||
|
||||
Time shows in WIB (UTC+7 for Indonesian users). Relative time shows "In Xh", "Xd ago", or "LIVE".
|
||||
Stats line (Fetched / Total / Match counts) only shown when `--raw` flag is used.
|
||||
|
||||
If a fetch is interrupted (API error/timeout), a `WARNING: Partial fetch` line appears showing data may be incomplete.
|
||||
|
||||
## Game Categories
|
||||
|
||||
@@ -96,7 +97,13 @@ Time shows in WIB (UTC+7 for Indonesian users). Relative time shows "In Xh", "Xd
|
||||
|
||||
## Filters Applied
|
||||
|
||||
Events are filtered to show only **tradeable** matches:
|
||||
The script classifies every event into one of two categories:
|
||||
|
||||
**Match Markets**: Events that are actual head-to-head matches (have `seriesSlug` + `gameId`, OR title contains " vs ").
|
||||
|
||||
**Non-match Markets**: Everything else — tournament futures, prop bets, player props, etc.
|
||||
|
||||
Tradeable match markets additionally require:
|
||||
- Must have seriesSlug + gameId (actual match, not tournament future)
|
||||
- ML volume > 0
|
||||
- acceptingOrders = true
|
||||
@@ -105,22 +112,19 @@ Events are filtered to show only **tradeable** matches:
|
||||
- ML bestAsk > 0.01 (market hasn't converged toward underdog)
|
||||
- BO2 matches that ended in a tie (1-1) are filtered out
|
||||
- Event endDate has not passed (API doesn't always close ML market promptly)
|
||||
- Match startTime is not more than 4 hours in the past (matches that already ended are filtered out)
|
||||
|
||||
Markets are filtered to only tradeable ones:
|
||||
- acceptingOrders = true
|
||||
- bestBid < 0.99
|
||||
- bestAsk > 0.01
|
||||
- closed = false
|
||||
Use `--raw` to disable the tradeable filter and see all match markets regardless of volume or open orders.
|
||||
|
||||
## Pagination
|
||||
|
||||
The script fetches **ALL pages** until the API runs out of results (up to 100 pages as a safety cap).
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- Exponential backoff: 2s → 4s → 8s → 16s → 32s
|
||||
- Max 5 retries before aborting
|
||||
|
||||
## Known Issues
|
||||
|
||||
- **BO2 matches that end in a tie (1-1)**: Filtered out by checking if all child_moneyline markets are closed.
|
||||
|
||||
## Odds Format
|
||||
|
||||
All odds are shown in **cents** format:
|
||||
|
||||
@@ -14,9 +14,7 @@ from datetime import datetime, timezone, timedelta
|
||||
# CONFIG
|
||||
# ============================================================
|
||||
|
||||
FETCH_PAGES = 4
|
||||
PAGE_SIZE = 5
|
||||
DISPLAY_MAX = 10
|
||||
PAGE_SIZE = 50
|
||||
MAX_RETRIES = 5
|
||||
INITIAL_RETRY_DELAY = 2 # exponential backoff starts at 2s
|
||||
|
||||
@@ -66,20 +64,29 @@ def fetch_page(q, page=1, max_retries=MAX_RETRIES, initial_delay=INITIAL_RETRY_D
|
||||
return None
|
||||
return None
|
||||
|
||||
def fetch_all_pages(q, num_pages=FETCH_PAGES):
|
||||
def fetch_all_pages(q, max_pages=100):
|
||||
"""
|
||||
Fetch ALL pages until pagination ends.
|
||||
max_pages is a safety cap to prevent infinite loops.
|
||||
"""
|
||||
all_events = []
|
||||
total_raw = 0
|
||||
for page in range(1, num_pages + 1):
|
||||
time.sleep(1)
|
||||
for page in range(1, max_pages + 1):
|
||||
time.sleep(0.2) # small delay between pages (API rate limit is generous)
|
||||
data = fetch_page(q, page)
|
||||
if data is None:
|
||||
break
|
||||
events = data.get("events", [])
|
||||
total_raw = data.get("pagination", {}).get("totalResults", "?")
|
||||
total_raw = data.get("pagination", {}).get("totalResults", 0)
|
||||
all_events.extend(events)
|
||||
if len(events) < PAGE_SIZE:
|
||||
# Stop when we get 0 events (no more pages),
|
||||
# OR when we've fetched >= total results
|
||||
if len(events) == 0:
|
||||
break
|
||||
return {"events": all_events, "total_raw": total_raw}
|
||||
if len(all_events) >= total_raw:
|
||||
break
|
||||
partial = (total_raw > 0 and len(all_events) < total_raw)
|
||||
return {"events": all_events, "total_raw": total_raw, "partial": partial}
|
||||
|
||||
# ============================================================
|
||||
# FILTERS
|
||||
@@ -88,6 +95,16 @@ def fetch_all_pages(q, num_pages=FETCH_PAGES):
|
||||
def is_match_market(e):
|
||||
return (e.get("seriesSlug") and e.get("gameId")) or " vs " in e.get("title", "")
|
||||
|
||||
def get_event_url(e):
|
||||
"""Return the correct Polymarket URL for an event.
|
||||
Match markets use /market/, non-match events use /event/.
|
||||
"""
|
||||
slug = e.get("slug", "")
|
||||
if is_match_market(e):
|
||||
return f"https://polymarket.com/market/{slug}"
|
||||
else:
|
||||
return f"https://polymarket.com/event/{slug}"
|
||||
|
||||
def get_ml_market(e):
|
||||
for m in e.get("markets", []):
|
||||
if m.get("sportsMarketType") == "moneyline":
|
||||
@@ -152,6 +169,20 @@ def is_tradeable_event(e):
|
||||
except:
|
||||
pass
|
||||
|
||||
# Filter: match has already started (startTime is in the past)
|
||||
start_str = e.get("startTime") or e.get("startDate", "")
|
||||
if start_str:
|
||||
try:
|
||||
start_dt = datetime.fromisoformat(start_str.replace('Z', '+00:00'))
|
||||
now = datetime.now(timezone.utc)
|
||||
if start_dt < now:
|
||||
# Check if it's recently started (within 4h) — consider those "live" still
|
||||
hours_ago = (now - start_dt).total_seconds() / 3600
|
||||
if hours_ago > 4:
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def is_tradeable_market(m):
|
||||
@@ -273,8 +304,22 @@ def get_match_time_str(e):
|
||||
except:
|
||||
return ""
|
||||
|
||||
def filter_events(events):
|
||||
return [e for e in events if is_match_market(e) and is_tradeable_event(e)]
|
||||
def filter_events(events, tradeable_only=True):
|
||||
"""
|
||||
Classify events into match_markets and non_match_markets.
|
||||
If tradeable_only=True, also filter out non-tradeable events.
|
||||
"""
|
||||
match_events = []
|
||||
non_match_events = []
|
||||
|
||||
for e in events:
|
||||
if is_match_market(e):
|
||||
if not tradeable_only or is_tradeable_event(e):
|
||||
match_events.append(e)
|
||||
else:
|
||||
non_match_events.append(e)
|
||||
|
||||
return match_events, non_match_events
|
||||
|
||||
def sort_events(events):
|
||||
return sorted(events, key=get_ml_volume, reverse=True)
|
||||
@@ -283,16 +328,20 @@ def sort_events(events):
|
||||
# BROWSE
|
||||
# ============================================================
|
||||
|
||||
def browse_events(q, display_max=DISPLAY_MAX):
|
||||
result = fetch_all_pages(q, FETCH_PAGES)
|
||||
def browse_events(q, matches_max=10, non_matches_max=10, tradeable_only=True):
|
||||
result = fetch_all_pages(q)
|
||||
events = result["events"]
|
||||
filtered = filter_events(events)
|
||||
sorted_events = sort_events(filtered)
|
||||
match_events, non_match_events = filter_events(events, tradeable_only)
|
||||
sorted_match = sort_events(match_events)
|
||||
return {
|
||||
"query": q,
|
||||
"total_raw": result["total_raw"],
|
||||
"total_filtered": len(filtered),
|
||||
"events": sorted_events[:display_max],
|
||||
"total_fetched": len(events),
|
||||
"total_match": len(match_events),
|
||||
"total_non_match": len(non_match_events),
|
||||
"match_events": sorted_match[:matches_max],
|
||||
"non_match_events": non_match_events[:non_matches_max],
|
||||
"partial": result.get("partial", False),
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
@@ -312,7 +361,7 @@ def format_event(e):
|
||||
"title": e.get("title", ""),
|
||||
"time_status": time_status,
|
||||
"time_urgency": urgency,
|
||||
"url": f"https://polymarket.com/market/{e.get('slug')}",
|
||||
"url": get_event_url(e),
|
||||
"livestream": e.get("resolutionSource"),
|
||||
"outcomes": outcomes,
|
||||
"prices": prices,
|
||||
@@ -335,7 +384,7 @@ def format_detail_event(e):
|
||||
return {
|
||||
"title": e.get("title", ""),
|
||||
"time_status": time_status,
|
||||
"url": f"https://polymarket.com/market/{e.get('slug')}",
|
||||
"url": get_event_url(e),
|
||||
"livestream": e.get("resolutionSource"),
|
||||
"outcomes": json.loads(ml.get("outcomes", "[]")) if ml else [],
|
||||
"prices": json.loads(ml.get("outcomePrices", "[]")) if ml else [],
|
||||
@@ -389,8 +438,8 @@ def get_start_time_wib(e):
|
||||
else:
|
||||
hours_until = delta.total_seconds() / 3600
|
||||
if hours_until < 1:
|
||||
mins = int(delta.total_seconds() / 60)
|
||||
rel_str = f"In {mins}m"
|
||||
mins_until = int(delta.total_seconds() / 60)
|
||||
rel_str = f"In {mins_until}m"
|
||||
elif hours_until < 24:
|
||||
rel_str = f"In {int(hours_until)}h"
|
||||
else:
|
||||
@@ -416,62 +465,82 @@ def get_tournament(title):
|
||||
return " - ".join(parts[1:]).strip()
|
||||
return ""
|
||||
|
||||
def print_browse(events, category, total_raw, total_filtered):
|
||||
def print_browse(match_events, non_match_events, category, total_raw, total_fetched, total_match, total_non_match, raw_mode=False, partial=False, non_matches_max=5, matches_only=False, non_matches_only=False):
|
||||
from datetime import datetime, timezone, timedelta
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
utc7 = timezone(timedelta(hours=7))
|
||||
now_utc7 = now_utc.astimezone(utc7)
|
||||
header_date = get_header_date()
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"=== {category.upper()} ===")
|
||||
print(f"{'='*60}")
|
||||
print(f"Query: '{GAME_CATEGORIES[category]}' | Total API: {total_raw} | Tradeable: {total_filtered}")
|
||||
print(f"\n=== {category.upper()}{' [RAW]' if raw_mode else ''} ===")
|
||||
print(f"Current time (WIB): {now_utc7.strftime('%H:%M WIB')} | {header_date}")
|
||||
|
||||
if not events:
|
||||
print(" No tradeable events found.")
|
||||
return
|
||||
if raw_mode:
|
||||
print(f"Fetched: {total_fetched} / Total API: {total_raw} | Match: {total_match} | Non-match: {total_non_match}")
|
||||
if partial:
|
||||
print(f"WARNING: Partial fetch (API error or timeout) — data may be incomplete")
|
||||
|
||||
for i, e in enumerate(events, 1):
|
||||
f = format_event(e)
|
||||
ml = get_ml_market(e)
|
||||
outcomes = json.loads(ml.get("outcomes", "[]")) if ml else []
|
||||
prices = json.loads(ml.get("outcomePrices", "[]")) if ml else []
|
||||
vol = f["volume"]
|
||||
title = f["title"]
|
||||
url = f["url"]
|
||||
start_time_wib, rel_time = get_start_time_wib(e)
|
||||
|
||||
team_a = outcomes[0] if len(outcomes) > 0 else "?"
|
||||
team_b = outcomes[1] if len(outcomes) > 1 else "?"
|
||||
odds_a = format_odds(float(prices[0])) if len(prices) > 0 else "?"
|
||||
odds_b = format_odds(float(prices[1])) if len(prices) > 1 else "?"
|
||||
|
||||
# Extract title without tournament for line 1
|
||||
# Title format: "Category: TeamA vs TeamB (BO/X) - Tournament"
|
||||
# We want: "Category: TeamA vs TeamB (BO/X)"
|
||||
if " - " in title:
|
||||
title_clean = title.split(" - ")[0].strip()
|
||||
# --- MATCH MARKETS ---
|
||||
if not matches_only and not non_matches_only:
|
||||
# Default: show both
|
||||
show_matches = True
|
||||
show_non_matches = True
|
||||
elif matches_only:
|
||||
show_matches = True
|
||||
show_non_matches = False
|
||||
else:
|
||||
show_matches = False
|
||||
show_non_matches = True
|
||||
|
||||
if show_matches:
|
||||
print(f"\nMATCH MARKETS")
|
||||
if not match_events:
|
||||
print(" No match markets found.")
|
||||
else:
|
||||
title_clean = title
|
||||
for i, e in enumerate(match_events, 1):
|
||||
f = format_event(e)
|
||||
ml = get_ml_market(e)
|
||||
outcomes = json.loads(ml.get("outcomes", "[]")) if ml else []
|
||||
prices = json.loads(ml.get("outcomePrices", "[]")) if ml else []
|
||||
vol = f["volume"]
|
||||
title = f["title"]
|
||||
url = f["url"]
|
||||
start_time_wib, rel_time = get_start_time_wib(e)
|
||||
|
||||
team_a = outcomes[0] if len(outcomes) > 0 else "?"
|
||||
team_b = outcomes[1] if len(outcomes) > 1 else "?"
|
||||
odds_a = format_odds(float(prices[0])) if len(prices) > 0 else "?"
|
||||
odds_b = format_odds(float(prices[1])) if len(prices) > 1 else "?"
|
||||
|
||||
if " - " in title:
|
||||
title_clean = title.split(" - ")[0].strip()
|
||||
else:
|
||||
title_clean = title
|
||||
|
||||
tournament = get_tournament(title)
|
||||
|
||||
print(f"\n {i}. [{title_clean}]({url})")
|
||||
print(f" {start_time_wib} | {rel_time}")
|
||||
print(f" Vol: ${vol:,.0f}")
|
||||
if tournament:
|
||||
print(f" Tournament: {tournament}")
|
||||
print(f" Odds: {team_a} {odds_a} | {odds_b} {team_b}")
|
||||
|
||||
# --- NON-MATCH MARKETS ---
|
||||
if show_non_matches and non_match_events:
|
||||
print(f"\nNON-MATCH MARKETS")
|
||||
|
||||
tournament = get_tournament(title)
|
||||
|
||||
# New format:
|
||||
# 1. [Title](url) <- title without tournament
|
||||
# 2. Date, Time WIB | Relative
|
||||
# (empty)
|
||||
# Vol: $XXX
|
||||
# Tournament: XXXXX
|
||||
# Odds: TeamA Xc | Xc TeamB
|
||||
print(f"\n {i}. [{title_clean}]({url})")
|
||||
print(f" {start_time_wib} | {rel_time}")
|
||||
print(f"")
|
||||
print(f" Vol: ${vol:,.0f}")
|
||||
if tournament:
|
||||
print(f" Tournament: {tournament}")
|
||||
print(f" Odds: {team_a} {odds_a} | {odds_b} {team_b}")
|
||||
for i, e in enumerate(non_match_events[:non_matches_max], 1):
|
||||
title = e.get("title", "?")
|
||||
url = get_event_url(e)
|
||||
start_time_wib, rel_time = get_start_time_wib(e)
|
||||
|
||||
total_vol = sum(float(m.get("volume", 0)) for m in e.get("markets", []))
|
||||
market_count = len(e.get("markets", []))
|
||||
|
||||
print(f"\n {i}. [{title}]({url})")
|
||||
print(f" {start_time_wib} | {rel_time}")
|
||||
print(f" Markets: {market_count} | Total Vol: ${total_vol:,.0f}")
|
||||
|
||||
def print_detail(e, detail):
|
||||
from datetime import datetime, timezone, timedelta
|
||||
@@ -479,9 +548,7 @@ def print_detail(e, detail):
|
||||
utc7 = timezone(timedelta(hours=7))
|
||||
now_utc7 = now_utc.astimezone(utc7)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"=== DETAIL: {detail['title'][:60]} ===")
|
||||
print(f"{'='*60}")
|
||||
print(f"\n{detail['title']}")
|
||||
print(f"URL: {detail['url']}")
|
||||
print(f"Livestream: {detail['livestream']}")
|
||||
|
||||
@@ -499,6 +566,143 @@ def print_detail(e, detail):
|
||||
print(f" Vol: ${m['volume']:,.0f} | {spread_str}")
|
||||
print(f" URL: {m['url']}")
|
||||
|
||||
# ============================================================
|
||||
# TELEGRAM
|
||||
# ============================================================
|
||||
|
||||
def send_to_telegram(match_events, non_match_events, category, matches_only=False, non_matches_only=False):
|
||||
"""Send browse results to Telegram. Reads BOT_TOKEN and CHAT_ID from environment."""
|
||||
import os
|
||||
bot_token = os.environ.get("BOT_TOKEN")
|
||||
chat_id = os.environ.get("CHAT_ID")
|
||||
if not bot_token or not chat_id:
|
||||
print("WARNING: BOT_TOKEN or CHAT_ID not set in environment. Skipping Telegram send.")
|
||||
return
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
utc7 = timezone(timedelta(hours=7))
|
||||
now_utc7 = now_utc.astimezone(utc7)
|
||||
header_date = now_utc7.strftime("%b %d, %Y")
|
||||
|
||||
# Determine sections to show
|
||||
show_matches = (not matches_only and not non_matches_only) or matches_only
|
||||
show_non_matches = (not matches_only and not non_matches_only) or non_matches_only
|
||||
|
||||
def send(text):
|
||||
result = subprocess.run(
|
||||
["curl", "-s", f"https://api.telegram.org/bot{bot_token}/sendMessage",
|
||||
"-d", f"chat_id={chat_id}",
|
||||
"-d", f"text={text}",
|
||||
"-d", "parse_mode=HTML",
|
||||
"-d", "disable_web_page_preview=true"],
|
||||
capture_output=True
|
||||
)
|
||||
resp = json.loads(result.stdout.decode())
|
||||
if resp.get("ok"):
|
||||
print(f" Sent msg {resp['result']['message_id']}")
|
||||
else:
|
||||
print(f" Error: {resp.get('description')}")
|
||||
|
||||
# Build sections
|
||||
lines = [f"<b>{category.upper()}</b> | {header_date}"]
|
||||
lines.append("")
|
||||
|
||||
if show_matches:
|
||||
lines.append("MATCH MARKETS")
|
||||
lines.append("")
|
||||
if not match_events:
|
||||
lines.append(" No match markets found.")
|
||||
else:
|
||||
for i, e in enumerate(match_events, 1):
|
||||
ml = get_ml_market(e)
|
||||
outcomes = json.loads(ml.get("outcomes", "[]")) if ml else []
|
||||
prices = json.loads(ml.get("outcomePrices", "[]")) if ml else []
|
||||
vol = get_ml_volume(e)
|
||||
title = e.get("title", "?")
|
||||
url = get_event_url(e)
|
||||
start_time_wib, rel_time = get_start_time_wib(e)
|
||||
team_a = outcomes[0] if len(outcomes) > 0 else "?"
|
||||
team_b = outcomes[1] if len(outcomes) > 1 else "?"
|
||||
odds_a = format_odds(float(prices[0])) if len(prices) > 0 else "?"
|
||||
odds_b = format_odds(float(prices[1])) if len(prices) > 1 else "?"
|
||||
tournament = get_tournament(title)
|
||||
title_clean = title.split(" - ")[0].strip() if " - " in title else title
|
||||
lines.append(f"<b>{i}.</b> <a href=\"{url}\">{title_clean}</a>")
|
||||
lines.append(f" {start_time_wib} | {rel_time}")
|
||||
lines.append(f" Vol: ${vol:,.0f}")
|
||||
if tournament:
|
||||
lines.append(f" Tournament: {tournament}")
|
||||
lines.append(f" Odds: {team_a} {odds_a} | {odds_b} {team_b}")
|
||||
lines.append("")
|
||||
lines.append("")
|
||||
|
||||
if show_non_matches:
|
||||
lines.append("NON-MATCH MARKETS")
|
||||
lines.append("")
|
||||
if not non_match_events:
|
||||
lines.append(" No non-match markets found.")
|
||||
else:
|
||||
for i, e in enumerate(non_match_events, 1):
|
||||
title = e.get("title", "?")
|
||||
url = get_event_url(e)
|
||||
start_time_wib, rel_time = get_start_time_wib(e)
|
||||
total_vol = sum(float(m.get("volume", 0)) for m in e.get("markets", []))
|
||||
market_count = len(e.get("markets", []))
|
||||
lines.append(f"<b>{i}.</b> <a href=\"{url}\">{title}</a>")
|
||||
lines.append(f" {start_time_wib} | {rel_time}")
|
||||
lines.append(f" Markets: {market_count} | Total Vol: ${total_vol:,.0f}")
|
||||
lines.append("")
|
||||
|
||||
# Chunk by 10 items (events), respecting 4096 char Telegram limit
|
||||
text = "\n".join(lines)
|
||||
if len(text) <= 4096:
|
||||
send(text)
|
||||
return
|
||||
|
||||
# Split into chunks of 10 events
|
||||
all_items = []
|
||||
in_match = True
|
||||
for line in lines:
|
||||
if line == "MATCH MARKETS":
|
||||
in_match = True
|
||||
elif line == "NON-MATCH MARKETS":
|
||||
in_match = False
|
||||
elif line.startswith("<b>") and ". " in line and "</a>" in line:
|
||||
all_items.append((in_match, line))
|
||||
|
||||
chunk = []
|
||||
chunk_len = 0
|
||||
chunk_num = 1
|
||||
|
||||
# Header is always first
|
||||
header = f"<b>{category.upper()}</b> | {header_date}\n"
|
||||
if show_matches:
|
||||
header += "\nMATCH MARKETS\n\n"
|
||||
if show_non_matches:
|
||||
header += "\nNON-MATCH MARKETS\n\n"
|
||||
|
||||
for is_match, item_line in all_items:
|
||||
test_chunk = chunk + [item_line, ""]
|
||||
test_text = header + "\n".join(chunk) + "\n".join(test_chunk)
|
||||
if len(test_text) > 4096 or len(chunk) >= 10:
|
||||
# Send current chunk
|
||||
msg = header + "\n".join(chunk)
|
||||
send(msg)
|
||||
chunk = [item_line, ""]
|
||||
header = f"<b>{category.upper()}</b> (cont.) | {header_date}\n"
|
||||
if show_matches and is_match:
|
||||
header += "\nMATCH MARKETS\n\n"
|
||||
elif show_non_matches and not is_match:
|
||||
header += "\nNON-MATCH MARKETS\n\n"
|
||||
else:
|
||||
chunk.extend([item_line, ""])
|
||||
|
||||
if chunk:
|
||||
msg = header + "\n".join(chunk)
|
||||
send(msg)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MAIN
|
||||
# ============================================================
|
||||
@@ -509,11 +713,25 @@ def main():
|
||||
choices=list(GAME_CATEGORIES.keys()),
|
||||
help="Game category to browse")
|
||||
parser.add_argument("--limit", type=int, default=5,
|
||||
help="Max events to show")
|
||||
help="Max events per section (match + non-match). Default: 5")
|
||||
parser.add_argument("--matches", type=int, default=None,
|
||||
help="Max match markets to show. Default: --limit")
|
||||
parser.add_argument("--non-matches", type=int, default=None,
|
||||
help="Max non-match markets to show. Default: --limit")
|
||||
parser.add_argument("--search", type=str, default=None,
|
||||
help="Free-text team/term search within the selected category. Overrides default query.")
|
||||
parser.add_argument("--matches-only", action="store_true",
|
||||
help="Show only match markets (suppress non-match section).")
|
||||
parser.add_argument("--non-matches-only", action="store_true",
|
||||
help="Show only non-match markets (suppress match section).")
|
||||
parser.add_argument("--list-categories", action="store_true",
|
||||
help="List available game categories and exit")
|
||||
parser.add_argument("--detail", type=int, default=1,
|
||||
help="Index of event (1-indexed) to show detailed markets for. Default: 1. Set to 0 to disable.")
|
||||
help="Index of match event (1-indexed) to show detailed markets. Default: 1. Set to 0 to disable.")
|
||||
parser.add_argument("--raw", action="store_true",
|
||||
help="Show all events without tradeable filter (for debugging).")
|
||||
parser.add_argument("--telegram", action="store_true",
|
||||
help="Send results to Telegram (BOT_TOKEN and CHAT_ID must be set in environment).")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list_categories:
|
||||
@@ -522,28 +740,53 @@ def main():
|
||||
print(f" - {name}")
|
||||
return
|
||||
|
||||
search_term = GAME_CATEGORIES[args.category]
|
||||
category_term = GAME_CATEGORIES[args.category]
|
||||
search_term = f"{category_term} {args.search}" if args.search else category_term
|
||||
tradeable_only = not args.raw
|
||||
matches_max = args.matches if args.matches is not None else args.limit
|
||||
non_matches_max = args.non_matches if args.non_matches is not None else args.limit
|
||||
|
||||
print(f"\nFetching {args.category} events...")
|
||||
if args.search:
|
||||
print(f"\nFetching {args.category} events matching '{args.search}'...")
|
||||
else:
|
||||
print(f"\nFetching {args.category} events...")
|
||||
|
||||
result = browse_events(search_term, display_max=args.limit)
|
||||
result = browse_events(search_term, matches_max=matches_max, non_matches_max=non_matches_max, tradeable_only=tradeable_only)
|
||||
|
||||
print_browse(
|
||||
result["events"],
|
||||
result["match_events"],
|
||||
result["non_match_events"],
|
||||
args.category,
|
||||
result["total_raw"],
|
||||
result["total_filtered"]
|
||||
result["total_fetched"],
|
||||
result["total_match"],
|
||||
result["total_non_match"],
|
||||
raw_mode=args.raw,
|
||||
partial=result.get("partial", False),
|
||||
non_matches_max=non_matches_max,
|
||||
matches_only=args.matches_only,
|
||||
non_matches_only=args.non_matches_only
|
||||
)
|
||||
|
||||
# Print detail for selected event if any
|
||||
if result["events"] and args.detail > 0:
|
||||
if result["match_events"] and args.detail > 0:
|
||||
print("\n")
|
||||
idx = args.detail - 1
|
||||
if idx < 0 or idx >= len(result["events"]):
|
||||
if idx < 0 or idx >= len(result["match_events"]):
|
||||
idx = 0
|
||||
detail_event = result["events"][idx]
|
||||
detail_event = result["match_events"][idx]
|
||||
detail = format_detail_event(detail_event)
|
||||
print_detail(detail_event, detail)
|
||||
|
||||
# Send to Telegram if requested
|
||||
if args.telegram:
|
||||
send_to_telegram(
|
||||
result["match_events"],
|
||||
result["non_match_events"],
|
||||
args.category,
|
||||
matches_only=args.matches_only,
|
||||
non_matches_only=args.non_matches_only
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user