polymarket-browse: add --search, --telegram, --matches-only, --non-matches-only flags; fix partial fetch warnings; clean up output formatting

This commit is contained in:
shoko
2026-03-24 13:26:29 +00:00
parent 46ecb38049
commit 88dc651232
2 changed files with 369 additions and 122 deletions

View File

@@ -12,73 +12,74 @@ Browse tradeable Polymarket prediction market events by game category.
**For Hermes Agent users:** **For Hermes Agent users:**
```bash ```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:** **For OpenClaw users:**
```bash ```bash
# Clone the repo # 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 # Copy skill to your OpenClaw skills folder
cp -r ~/jujutsu-skills/skills/polymarket-browse ~/.openclaw/skills/ 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 ```bash
# Clone the repo # Ask your agent to install it for you, or add to config.yaml:
git clone https://git.fbrns.co/shoko/jujutsu-skills.git ~/jujutsu-skills hermes mcp add polymarket https://docs.polymarket.com/mcp
# Copy skill to your Hermes skills folder
cp -r ~/jujutsu-skills/skills/polymarket-browse ~/.hermes/skills/
``` ```
## Usage ## 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 ## Arguments
- `--category` : Game category to browse. Options: All Esports, Counter Strike, League of Legends, Dota 2, Valorant, NBA, NFL, UFC, Tennis (default: Counter Strike) - `--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) - `--limit` : Max events per section (match + non-match). Default: 5
- `--detail` : Index of event (1-indexed) to show detailed markets for. Defaults to 1 (first event). Set to 0 to disable. - `--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 - `--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 ## Output Format
Each event displays in a **6-line format**: Output is split into two sections:
``` ```
=== DOTA 2 === === COUNTER STRIKE ===
Query: 'Dota2' | Total API: 54 | Tradeable: 3 Current time (WIB): 17:00 WIB | Mar 24, 2026
Current time (WIB): 13:58 WIB | Mar 24, 2026
1. [Dota 2: Yakult Brothers vs BetBoom Team (BO2)](https://polymarket.com/market/...) MATCH MARKETS
Mar 24, 19:00 WIB | In 5h
Vol: $4,952 1. [Counter-Strike: TheMongolz vs Spirit (BO3)](https://polymarket.com/market/...)
Tournament: ESL One Birmingham Group A Mar 24, 03:45 WIB | LIVE
Odds: Yakult Brothers 29c | 71c BetBoom Team 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/...) NON-MATCH MARKETS
Mar 24, 19:00 WIB | In 5h
Vol: $3,944 1. [Blast Open Rotterdam 2026: Winner](https://polymarket.com/event/...)
Tournament: ESL One Birmingham Group A Feb 28, 04:43 WIB | 24d ago
Odds: GamerLegion 28c | 72c Team Yandex Markets: 17 | Total Vol: $958,116
``` ```
Format per event: **Match Markets** are actual head-to-head matches with moneyline odds (sorted by volume).
- **Line 1**: `[number]. [Title](url)` — title without tournament name **Non-match Markets** are tournament futures, props, and other markets without direct match odds.
- **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]`
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 ## Game Categories
@@ -96,7 +97,13 @@ Time shows in WIB (UTC+7 for Indonesian users). Relative time shows "In Xh", "Xd
## Filters Applied ## 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) - Must have seriesSlug + gameId (actual match, not tournament future)
- ML volume > 0 - ML volume > 0
- acceptingOrders = true - acceptingOrders = true
@@ -105,22 +112,19 @@ Events are filtered to show only **tradeable** matches:
- ML bestAsk > 0.01 (market hasn't converged toward underdog) - ML bestAsk > 0.01 (market hasn't converged toward underdog)
- BO2 matches that ended in a tie (1-1) are filtered out - 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) - 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: Use `--raw` to disable the tradeable filter and see all match markets regardless of volume or open orders.
- acceptingOrders = true
- bestBid < 0.99 ## Pagination
- bestAsk > 0.01
- closed = false The script fetches **ALL pages** until the API runs out of results (up to 100 pages as a safety cap).
## Rate Limiting ## Rate Limiting
- Exponential backoff: 2s → 4s → 8s → 16s → 32s - Exponential backoff: 2s → 4s → 8s → 16s → 32s
- Max 5 retries before aborting - 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 ## Odds Format
All odds are shown in **cents** format: All odds are shown in **cents** format:

View File

@@ -14,9 +14,7 @@ from datetime import datetime, timezone, timedelta
# CONFIG # CONFIG
# ============================================================ # ============================================================
FETCH_PAGES = 4 PAGE_SIZE = 50
PAGE_SIZE = 5
DISPLAY_MAX = 10
MAX_RETRIES = 5 MAX_RETRIES = 5
INITIAL_RETRY_DELAY = 2 # exponential backoff starts at 2s 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
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 = [] all_events = []
total_raw = 0 total_raw = 0
for page in range(1, num_pages + 1): for page in range(1, max_pages + 1):
time.sleep(1) time.sleep(0.2) # small delay between pages (API rate limit is generous)
data = fetch_page(q, page) data = fetch_page(q, page)
if data is None: if data is None:
break break
events = data.get("events", []) events = data.get("events", [])
total_raw = data.get("pagination", {}).get("totalResults", "?") total_raw = data.get("pagination", {}).get("totalResults", 0)
all_events.extend(events) 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 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 # FILTERS
@@ -88,6 +95,16 @@ def fetch_all_pages(q, num_pages=FETCH_PAGES):
def is_match_market(e): def is_match_market(e):
return (e.get("seriesSlug") and e.get("gameId")) or " vs " in e.get("title", "") 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): def get_ml_market(e):
for m in e.get("markets", []): for m in e.get("markets", []):
if m.get("sportsMarketType") == "moneyline": if m.get("sportsMarketType") == "moneyline":
@@ -152,6 +169,20 @@ def is_tradeable_event(e):
except: except:
pass 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 return True
def is_tradeable_market(m): def is_tradeable_market(m):
@@ -273,8 +304,22 @@ def get_match_time_str(e):
except: except:
return "" return ""
def filter_events(events): def filter_events(events, tradeable_only=True):
return [e for e in events if is_match_market(e) and is_tradeable_event(e)] """
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): def sort_events(events):
return sorted(events, key=get_ml_volume, reverse=True) return sorted(events, key=get_ml_volume, reverse=True)
@@ -283,16 +328,20 @@ def sort_events(events):
# BROWSE # BROWSE
# ============================================================ # ============================================================
def browse_events(q, display_max=DISPLAY_MAX): def browse_events(q, matches_max=10, non_matches_max=10, tradeable_only=True):
result = fetch_all_pages(q, FETCH_PAGES) result = fetch_all_pages(q)
events = result["events"] events = result["events"]
filtered = filter_events(events) match_events, non_match_events = filter_events(events, tradeable_only)
sorted_events = sort_events(filtered) sorted_match = sort_events(match_events)
return { return {
"query": q, "query": q,
"total_raw": result["total_raw"], "total_raw": result["total_raw"],
"total_filtered": len(filtered), "total_fetched": len(events),
"events": sorted_events[:display_max], "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", ""), "title": e.get("title", ""),
"time_status": time_status, "time_status": time_status,
"time_urgency": urgency, "time_urgency": urgency,
"url": f"https://polymarket.com/market/{e.get('slug')}", "url": get_event_url(e),
"livestream": e.get("resolutionSource"), "livestream": e.get("resolutionSource"),
"outcomes": outcomes, "outcomes": outcomes,
"prices": prices, "prices": prices,
@@ -335,7 +384,7 @@ def format_detail_event(e):
return { return {
"title": e.get("title", ""), "title": e.get("title", ""),
"time_status": time_status, "time_status": time_status,
"url": f"https://polymarket.com/market/{e.get('slug')}", "url": get_event_url(e),
"livestream": e.get("resolutionSource"), "livestream": e.get("resolutionSource"),
"outcomes": json.loads(ml.get("outcomes", "[]")) if ml else [], "outcomes": json.loads(ml.get("outcomes", "[]")) if ml else [],
"prices": json.loads(ml.get("outcomePrices", "[]")) if ml else [], "prices": json.loads(ml.get("outcomePrices", "[]")) if ml else [],
@@ -389,8 +438,8 @@ def get_start_time_wib(e):
else: else:
hours_until = delta.total_seconds() / 3600 hours_until = delta.total_seconds() / 3600
if hours_until < 1: if hours_until < 1:
mins = int(delta.total_seconds() / 60) mins_until = int(delta.total_seconds() / 60)
rel_str = f"In {mins}m" rel_str = f"In {mins_until}m"
elif hours_until < 24: elif hours_until < 24:
rel_str = f"In {int(hours_until)}h" rel_str = f"In {int(hours_until)}h"
else: else:
@@ -416,24 +465,39 @@ def get_tournament(title):
return " - ".join(parts[1:]).strip() return " - ".join(parts[1:]).strip()
return "" 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 from datetime import datetime, timezone, timedelta
now_utc = datetime.now(timezone.utc) now_utc = datetime.now(timezone.utc)
utc7 = timezone(timedelta(hours=7)) utc7 = timezone(timedelta(hours=7))
now_utc7 = now_utc.astimezone(utc7) now_utc7 = now_utc.astimezone(utc7)
header_date = get_header_date() header_date = get_header_date()
print(f"\n{'='*60}") print(f"\n=== {category.upper()}{' [RAW]' if raw_mode else ''} ===")
print(f"=== {category.upper()} ===")
print(f"{'='*60}")
print(f"Query: '{GAME_CATEGORIES[category]}' | Total API: {total_raw} | Tradeable: {total_filtered}")
print(f"Current time (WIB): {now_utc7.strftime('%H:%M WIB')} | {header_date}") print(f"Current time (WIB): {now_utc7.strftime('%H:%M WIB')} | {header_date}")
if not events: if raw_mode:
print(" No tradeable events found.") print(f"Fetched: {total_fetched} / Total API: {total_raw} | Match: {total_match} | Non-match: {total_non_match}")
return if partial:
print(f"WARNING: Partial fetch (API error or timeout) — data may be incomplete")
for i, e in enumerate(events, 1): # --- 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:
for i, e in enumerate(match_events, 1):
f = format_event(e) f = format_event(e)
ml = get_ml_market(e) ml = get_ml_market(e)
outcomes = json.loads(ml.get("outcomes", "[]")) if ml else [] outcomes = json.loads(ml.get("outcomes", "[]")) if ml else []
@@ -448,9 +512,6 @@ def print_browse(events, category, total_raw, total_filtered):
odds_a = format_odds(float(prices[0])) if len(prices) > 0 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 "?" 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: if " - " in title:
title_clean = title.split(" - ")[0].strip() title_clean = title.split(" - ")[0].strip()
else: else:
@@ -458,30 +519,36 @@ def print_browse(events, category, total_raw, total_filtered):
tournament = get_tournament(title) 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"\n {i}. [{title_clean}]({url})")
print(f" {start_time_wib} | {rel_time}") print(f" {start_time_wib} | {rel_time}")
print(f"")
print(f" Vol: ${vol:,.0f}") print(f" Vol: ${vol:,.0f}")
if tournament: if tournament:
print(f" Tournament: {tournament}") print(f" Tournament: {tournament}")
print(f" Odds: {team_a} {odds_a} | {odds_b} {team_b}") 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")
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): def print_detail(e, detail):
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
now_utc = datetime.now(timezone.utc) now_utc = datetime.now(timezone.utc)
utc7 = timezone(timedelta(hours=7)) utc7 = timezone(timedelta(hours=7))
now_utc7 = now_utc.astimezone(utc7) now_utc7 = now_utc.astimezone(utc7)
print(f"\n{'='*60}") print(f"\n{detail['title']}")
print(f"=== DETAIL: {detail['title'][:60]} ===")
print(f"{'='*60}")
print(f"URL: {detail['url']}") print(f"URL: {detail['url']}")
print(f"Livestream: {detail['livestream']}") print(f"Livestream: {detail['livestream']}")
@@ -499,6 +566,143 @@ def print_detail(e, detail):
print(f" Vol: ${m['volume']:,.0f} | {spread_str}") print(f" Vol: ${m['volume']:,.0f} | {spread_str}")
print(f" URL: {m['url']}") 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 # MAIN
# ============================================================ # ============================================================
@@ -509,11 +713,25 @@ def main():
choices=list(GAME_CATEGORIES.keys()), choices=list(GAME_CATEGORIES.keys()),
help="Game category to browse") help="Game category to browse")
parser.add_argument("--limit", type=int, default=5, 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", parser.add_argument("--list-categories", action="store_true",
help="List available game categories and exit") help="List available game categories and exit")
parser.add_argument("--detail", type=int, default=1, 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() args = parser.parse_args()
if args.list_categories: if args.list_categories:
@@ -522,28 +740,53 @@ def main():
print(f" - {name}") print(f" - {name}")
return 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
if args.search:
print(f"\nFetching {args.category} events matching '{args.search}'...")
else:
print(f"\nFetching {args.category} events...") 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( print_browse(
result["events"], result["match_events"],
result["non_match_events"],
args.category, args.category,
result["total_raw"], 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 # Print detail for selected event if any
if result["events"] and args.detail > 0: if result["match_events"] and args.detail > 0:
print("\n") print("\n")
idx = args.detail - 1 idx = args.detail - 1
if idx < 0 or idx >= len(result["events"]): if idx < 0 or idx >= len(result["match_events"]):
idx = 0 idx = 0
detail_event = result["events"][idx] detail_event = result["match_events"][idx]
detail = format_detail_event(detail_event) detail = format_detail_event(detail_event)
print_detail(detail_event, detail) 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__": if __name__ == "__main__":
main() main()