Spaces:
Running
Running
| """ | |
| NationStates High-Value Trading Card Tracker β Gradio Web App | |
| ============================================================== | |
| Deployed to Hugging Face Spaces. Uses core.py for all shared logic. | |
| COMPLIANCE: This tool is purely read-only. No automated trades. | |
| """ | |
| import gradio as gr | |
| import sqlite3 | |
| import time | |
| import os | |
| import pandas as pd | |
| from datetime import datetime | |
| from core import ( | |
| init_db, fetch_card_info, fetch_card_market, fetch_full_market, | |
| fetch_card_trades, check_nation_exists, scan_low_id_cards, | |
| scan_cte_cards, scan_historical_cards, check_target_prices, | |
| record_price_snapshot, calculate_trade_score, suggest_bid_price, | |
| SEASON, RATE_LIMIT_SECONDS, THRESHOLDS, HISTORICAL_NAMES, USER_AGENT | |
| ) | |
| # ========================================== | |
| # CONFIGURATION | |
| # ========================================== | |
| DB_PATH = "/tmp/cards_tracker.db" | |
| # ========================================== | |
| # SMART TRADING UI HANDLERS | |
| # ========================================== | |
| def run_smart_analysis(max_cards): | |
| """Analyze all targets and score them for trading opportunity.""" | |
| init_db(DB_PATH) | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| c.execute(""" | |
| SELECT card_id, season, name, rarity, flag_reason, lowest_ask | |
| FROM targets | |
| ORDER BY CASE rarity WHEN 'legendary' THEN 1 WHEN 'epic' THEN 2 ELSE 3 END | |
| LIMIT ? | |
| """, (int(max_cards),)) | |
| targets = c.fetchall() | |
| conn.close() | |
| if not targets: | |
| return "No targets found. Run a scan first.", pd.DataFrame() | |
| results = [] | |
| errors = 0 | |
| for card_id, season, name, rarity, flag_reason, lowest_ask in targets: | |
| snapshot = record_price_snapshot(DB_PATH, card_id, season) | |
| if snapshot.get("error"): | |
| errors += 1 | |
| trades = fetch_card_trades(card_id, season) | |
| if trades: | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| for t in trades: | |
| c.execute("""INSERT OR IGNORE INTO trade_history (card_id, season, timestamp, price, buyer, seller) | |
| VALUES (?, ?, ?, ?, ?, ?)""", | |
| (card_id, season, t["timestamp"], t["price"], t["buyer"], t["seller"])) | |
| conn.commit() | |
| conn.close() | |
| score = calculate_trade_score(DB_PATH, card_id, season) | |
| bid_suggestions = suggest_bid_price(DB_PATH, card_id, season) | |
| suggested_bid = "" | |
| if bid_suggestions: | |
| if "aggressive_bid" in bid_suggestions: | |
| suggested_bid = f"{bid_suggestions['aggressive_bid']:.2f}" | |
| elif "below_mv_20pct" in bid_suggestions: | |
| suggested_bid = f"{bid_suggestions['below_mv_20pct']:.2f}" | |
| results.append({ | |
| "Card ID": card_id, | |
| "Name": name, | |
| "Rarity": rarity, | |
| "Reason": flag_reason or "", | |
| "Ask": f"{snapshot['lowest_ask']:.2f}" if snapshot["lowest_ask"] else "β", | |
| "Bid": f"{snapshot['highest_bid']:.2f}" if snapshot["highest_bid"] else "β", | |
| "Score": score or 0, | |
| "Suggested Bid": suggested_bid, | |
| "URL": f"https://www.nationstates.net/page=deck/card={card_id}/season={season}", | |
| }) | |
| results.sort(key=lambda x: x["Score"], reverse=True) | |
| df = pd.DataFrame(results) | |
| top_buys = [r for r in results if r["Score"] >= 70] | |
| summary = f"**Analyzed {len(results)} cards.**\n\n" | |
| if errors: | |
| summary += f"β οΈ {errors} API errors encountered during analysis.\n\n" | |
| if top_buys: | |
| summary += f"π― **{len(top_buys)} HIGH-OPPORTUNITY cards (score β₯ 70):**\n\n" | |
| for r in top_buys[:10]: | |
| summary += f"- **{r['Name']}** (ID:{r['Card ID']}) β {r['Rarity']} | Score: **{r['Score']}** | Ask: {r['Ask']} | Suggest bid: {r['Suggested Bid']}\n" | |
| else: | |
| summary += "No high-opportunity cards found at this time. Keep monitoring." | |
| return summary, df | |
| def run_card_deep_dive(card_id): | |
| """Deep analysis of a single card.""" | |
| init_db(DB_PATH) | |
| card_id = int(card_id) | |
| card_info = fetch_card_info(card_id, SEASON) | |
| if not card_info: | |
| return "β Card not found. The API may be unavailable or the card ID is invalid.", pd.DataFrame() | |
| market = fetch_full_market(card_id, SEASON) | |
| trades = fetch_card_trades(card_id, SEASON) | |
| # Store data | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| c.execute("INSERT OR REPLACE INTO cards VALUES (?, ?, ?, ?, ?, ?, ?)", | |
| (card_info["card_id"], card_info["season"], card_info["name"], | |
| card_info["rarity"], card_info["region"], card_info["market_value"], card_info.get("flag", ""))) | |
| c.execute("""INSERT INTO price_history (card_id, season, timestamp, lowest_ask, highest_bid, market_value, num_asks, num_bids) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", | |
| (card_id, SEASON, datetime.now().isoformat(), | |
| market["asks"][0]["price"] if market["asks"] else None, | |
| market["bids"][0]["price"] if market["bids"] else None, | |
| card_info["market_value"], len(market["asks"]), len(market["bids"]))) | |
| for t in trades: | |
| c.execute("""INSERT OR IGNORE INTO trade_history (card_id, season, timestamp, price, buyer, seller) | |
| VALUES (?, ?, ?, ?, ?, ?)""", | |
| (card_id, SEASON, t["timestamp"], t["price"], t["buyer"], t["seller"])) | |
| conn.commit() | |
| conn.close() | |
| score = calculate_trade_score(DB_PATH, card_id, SEASON) | |
| bid_suggestions = suggest_bid_price(DB_PATH, card_id, SEASON) | |
| # Build report | |
| report = f"## π Deep Dive: {card_info['name']} (ID: {card_id})\n\n" | |
| report += f"**Rarity:** {card_info['rarity'].upper()} | **Region:** {card_info['region']} | **Market Value:** {card_info['market_value']:.2f}\n\n" | |
| if market.get("error"): | |
| report += f"β οΈ *Market data may be incomplete: {market['error']}*\n\n" | |
| report += f"### Market State\n" | |
| if market["asks"]: | |
| report += f"- **Lowest Ask:** {market['asks'][0]['price']:.2f} ({len(market['asks'])} total asks)\n" | |
| else: | |
| report += f"- **No asks** (nobody selling)\n" | |
| if market["bids"]: | |
| report += f"- **Highest Bid:** {market['bids'][0]['price']:.2f} ({len(market['bids'])} total bids)\n" | |
| else: | |
| report += f"- **No bids** (nobody buying)\n" | |
| if market["asks"] and market["bids"]: | |
| spread = market["asks"][0]["price"] - market["bids"][0]["price"] | |
| spread_pct = (spread / market["asks"][0]["price"]) * 100 | |
| report += f"- **Spread:** {spread:.2f} ({spread_pct:.0f}%)\n" | |
| report += f"\n### Trading Score: **{score}/100**\n" | |
| if score and score >= 70: | |
| report += "π― **HIGH OPPORTUNITY** β Strong buy signal\n" | |
| elif score and score >= 50: | |
| report += "β‘ **MODERATE** β Worth watching\n" | |
| else: | |
| report += "βΈοΈ **LOW** β Not ideal timing\n" | |
| report += f"\n### Suggested Bid Prices\n" | |
| if bid_suggestions: | |
| for label, price in bid_suggestions.items(): | |
| nice_label = label.replace("_", " ").title() | |
| report += f"- **{nice_label}:** {price:.2f}\n" | |
| else: | |
| report += "- Not enough data for suggestions yet.\n" | |
| report += f"\n### Recent Trades ({len(trades)} found)\n" | |
| trade_data = [] | |
| if trades: | |
| for t in trades[:10]: | |
| ts = t["timestamp"] | |
| try: | |
| ts_display = datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %H:%M") if ts.isdigit() else ts[:16] | |
| except (ValueError, TypeError): | |
| ts_display = str(ts)[:16] | |
| trade_data.append({ | |
| "Date": ts_display, | |
| "Price": f"{t['price']:.2f}", | |
| "Buyer": t["buyer"], | |
| "Seller": t["seller"], | |
| }) | |
| report += f"\n[π View on NS](https://www.nationstates.net/page=deck/card={card_id}/season={SEASON})\n" | |
| df = pd.DataFrame(trade_data) if trade_data else pd.DataFrame(columns=["Date", "Price", "Buyer", "Seller"]) | |
| return report, df | |
| def add_to_watchlist(card_id, max_bid, notes): | |
| """Add a card to the watchlist.""" | |
| init_db(DB_PATH) | |
| card_id = int(card_id) | |
| max_bid = float(max_bid) if max_bid else 0 | |
| card_info = fetch_card_info(card_id, SEASON) | |
| if not card_info: | |
| return "β Card not found. Check the ID or try again later.", get_watchlist() | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| c.execute("""INSERT OR REPLACE INTO watchlist (card_id, season, name, rarity, max_bid, notes, added_at) | |
| VALUES (?, ?, ?, ?, ?, ?, ?)""", | |
| (card_id, SEASON, card_info["name"], card_info["rarity"], max_bid, notes, datetime.now().isoformat())) | |
| conn.commit() | |
| conn.close() | |
| return f"β Added **{card_info['name']}** (ID:{card_id}) to watchlist. Max bid: {max_bid:.2f}", get_watchlist() | |
| def remove_from_watchlist(card_id): | |
| """Remove a card from the watchlist.""" | |
| init_db(DB_PATH) | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| c.execute("DELETE FROM watchlist WHERE card_id = ? AND season = ?", (int(card_id), SEASON)) | |
| conn.commit() | |
| conn.close() | |
| return f"β Removed card {card_id} from watchlist.", get_watchlist() | |
| def get_watchlist(): | |
| """Get current watchlist.""" | |
| init_db(DB_PATH) | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| c.execute("""SELECT card_id, name, rarity, max_bid, notes, added_at FROM watchlist | |
| ORDER BY CASE rarity WHEN 'legendary' THEN 1 WHEN 'epic' THEN 2 ELSE 3 END""") | |
| rows = c.fetchall() | |
| conn.close() | |
| if rows: | |
| return pd.DataFrame(rows, columns=["Card ID", "Name", "Rarity", "Max Bid", "Notes", "Added"]) | |
| return pd.DataFrame(columns=["Card ID", "Name", "Rarity", "Max Bid", "Notes", "Added"]) | |
| def check_watchlist_alerts(): | |
| """Check watchlist cards for buy opportunities.""" | |
| init_db(DB_PATH) | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| c.execute("SELECT card_id, season, name, rarity, max_bid FROM watchlist") | |
| items = c.fetchall() | |
| conn.close() | |
| if not items: | |
| return "Watchlist is empty. Add cards first.", pd.DataFrame() | |
| alerts = [] | |
| errors = 0 | |
| for card_id, season, name, rarity, max_bid in items: | |
| market = fetch_full_market(card_id, season) | |
| if market.get("error"): | |
| errors += 1 | |
| lowest_ask = market["asks"][0]["price"] if market["asks"] else None | |
| if lowest_ask and max_bid and lowest_ask <= max_bid: | |
| alerts.append({ | |
| "Card ID": card_id, | |
| "Name": name, | |
| "Rarity": rarity, | |
| "Lowest Ask": f"{lowest_ask:.2f}", | |
| "Your Max Bid": f"{max_bid:.2f}", | |
| "Status": "π¨ BUY NOW", | |
| "URL": f"https://www.nationstates.net/page=deck/card={card_id}/season={season}", | |
| }) | |
| elif lowest_ask: | |
| alerts.append({ | |
| "Card ID": card_id, | |
| "Name": name, | |
| "Rarity": rarity, | |
| "Lowest Ask": f"{lowest_ask:.2f}", | |
| "Your Max Bid": f"{max_bid:.2f}" if max_bid else "β", | |
| "Status": "β³ Waiting", | |
| "URL": f"https://www.nationstates.net/page=deck/card={card_id}/season={season}", | |
| }) | |
| else: | |
| alerts.append({ | |
| "Card ID": card_id, | |
| "Name": name, | |
| "Rarity": rarity, | |
| "Lowest Ask": "No asks", | |
| "Your Max Bid": f"{max_bid:.2f}" if max_bid else "β", | |
| "Status": "π No sellers", | |
| "URL": f"https://www.nationstates.net/page=deck/card={card_id}/season={season}", | |
| }) | |
| df = pd.DataFrame(alerts) | |
| buy_now = [a for a in alerts if "BUY NOW" in a["Status"]] | |
| summary = f"**Checked {len(items)} watchlist cards.**\n\n" | |
| if errors: | |
| summary += f"β οΈ {errors} API errors encountered.\n\n" | |
| if buy_now: | |
| summary += f"π¨ **{len(buy_now)} cards available at or below your max bid!**\n\n" | |
| for a in buy_now: | |
| summary += f"- **{a['Name']}** β Ask: {a['Lowest Ask']} β€ Your max: {a['Your Max Bid']} β [π BUY]({a['URL']})\n" | |
| else: | |
| summary += "No cards at your target price yet. Keep checking." | |
| return summary, df | |
| # ========================================== | |
| # SCAN UI HANDLERS | |
| # ========================================== | |
| def run_low_id_scan(max_id): | |
| """UI handler for Low ID scan.""" | |
| init_db(DB_PATH) | |
| found = scan_low_id_cards(DB_PATH, int(max_id)) | |
| if found: | |
| df = pd.DataFrame(found) | |
| df["URL"] = df["card_id"].apply(lambda x: f"https://www.nationstates.net/page=deck/card={x}/season={SEASON}") | |
| df = df[["card_id", "name", "rarity", "region", "market_value", "URL"]] | |
| df.columns = ["Card ID", "Name", "Rarity", "Region", "Market Value", "URL"] | |
| return f"β Found **{len(found)}** Legendary/Epic cards with ID β€ {int(max_id)}", df | |
| return "No Legendary/Epic cards found in that range.", pd.DataFrame(columns=["Card ID", "Name", "Rarity", "Region", "Market Value", "URL"]) | |
| def run_cte_scan(): | |
| """UI handler for CTE scan.""" | |
| init_db(DB_PATH) | |
| cte = scan_cte_cards(DB_PATH) | |
| if cte: | |
| df = pd.DataFrame(cte) | |
| return f"β Found **{len(cte)}** CTE (dead nation) cards", df | |
| return "No CTE cards found (all nations still active or no cards in DB). Run a Low ID scan first.", pd.DataFrame(columns=["card_id", "name", "rarity"]) | |
| def run_historical_scan(): | |
| """UI handler for Historical name scan.""" | |
| init_db(DB_PATH) | |
| hist = scan_historical_cards(DB_PATH) | |
| if hist: | |
| df = pd.DataFrame(hist) | |
| return f"β Found **{len(hist)}** cards matching historical names", df | |
| return "No historical name matches found. Run a Low ID scan first to populate the card database.", pd.DataFrame(columns=["card_id", "name", "rarity", "match"]) | |
| def run_price_check(max_checks): | |
| """UI handler for price check.""" | |
| init_db(DB_PATH) | |
| alerts, errors = check_target_prices(DB_PATH, int(max_checks)) | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| c.execute(""" | |
| SELECT card_id, name, rarity, flag_reason, lowest_ask, last_checked, card_url | |
| FROM targets WHERE last_checked IS NOT NULL | |
| ORDER BY lowest_ask ASC | |
| """) | |
| rows = c.fetchall() | |
| conn.close() | |
| df = pd.DataFrame(rows, columns=["Card ID", "Name", "Rarity", "Flag Reason", "Ask Price", "Last Checked", "URL"]) | |
| alert_text = f"**Checked prices for targets.**\n\n" | |
| if errors: | |
| alert_text += f"β οΈ {errors} API errors encountered (some cards may have incomplete data).\n\n" | |
| if alerts: | |
| alert_text += f"π¨ **{len(alerts)} UNDERVALUED CARDS!**\n\n" | |
| for a in alerts: | |
| alert_text += f"- **{a['name']}** (ID: {a['card_id']}) β {a['rarity'].upper()}\n" | |
| alert_text += f" Reason: {a['reason']} | Ask: **{a['ask']:.2f}** (threshold: {a['threshold']})\n" | |
| alert_text += f" [π Buy Link]({a['url']})\n\n" | |
| else: | |
| alert_text += "β No undervalued cards below threshold." | |
| return alert_text, df | |
| def get_all_targets(): | |
| """Get all current targets from DB.""" | |
| init_db(DB_PATH) | |
| conn = sqlite3.connect(DB_PATH) | |
| c = conn.cursor() | |
| c.execute(""" | |
| SELECT card_id, name, rarity, flag_reason, lowest_ask, last_checked, card_url | |
| FROM targets | |
| ORDER BY CASE rarity WHEN 'legendary' THEN 1 WHEN 'epic' THEN 2 ELSE 3 END, card_id | |
| """) | |
| rows = c.fetchall() | |
| conn.close() | |
| if rows: | |
| return pd.DataFrame(rows, columns=["Card ID", "Name", "Rarity", "Flag Reason", "Ask Price", "Last Checked", "URL"]) | |
| return pd.DataFrame(columns=["Card ID", "Name", "Rarity", "Flag Reason", "Ask Price", "Last Checked", "URL"]) | |
| # ========================================== | |
| # BUILD APP | |
| # ========================================== | |
| with gr.Blocks(title="NS High-Value Card Tracker", theme=gr.themes.Soft()) as app: | |
| gr.Markdown("# π NationStates High-Value Trading Card Tracker") | |
| gr.Markdown("Identifies elite-tier undervalued cards via the NationStates API. **No automated trades** β manual purchase only via links.") | |
| gr.Markdown(f"*Season {SEASON} | Rate limit: {RATE_LIMIT_SECONDS}s/request | Compliance: Read-only*") | |
| with gr.Tab("π Low ID Scan"): | |
| gr.Markdown("Scan cards by ID range to find Legendary/Epic cards with low IDs (rare & valuable).") | |
| max_id_input = gr.Number(label="Scan cards 1 to:", value=10000, minimum=1, maximum=10000, precision=0) | |
| gr.Markdown("*β±οΈ Estimated time: ~1.2 seconds per card ID*") | |
| scan_btn = gr.Button("π Start Low ID Scan", variant="primary") | |
| scan_status = gr.Markdown("") | |
| scan_results = gr.Dataframe(label="Found Cards", interactive=False) | |
| scan_btn.click(fn=run_low_id_scan, inputs=[max_id_input], outputs=[scan_status, scan_results]) | |
| with gr.Tab("π CTE Scan"): | |
| gr.Markdown("Check if flagged cards belong to dead/deleted nations (Ceased to Exist). These are scarcer and more valuable.") | |
| gr.Markdown("*Requires Low ID scan to be run first to populate the card database.*") | |
| cte_btn = gr.Button("π Check for CTE Cards", variant="primary") | |
| cte_status = gr.Markdown("") | |
| cte_results = gr.Dataframe(label="CTE Cards", interactive=False) | |
| cte_btn.click(fn=run_cte_scan, outputs=[cte_status, cte_results]) | |
| with gr.Tab("π Historical Names"): | |
| gr.Markdown("Find cards matching famous NationStates names (regions, moderators, historic players).") | |
| gr.Markdown(f"*Monitoring {len(HISTORICAL_NAMES)} names: {', '.join(HISTORICAL_NAMES[:10])}...*") | |
| hist_btn = gr.Button("π Scan Historical Names", variant="primary") | |
| hist_status = gr.Markdown("") | |
| hist_results = gr.Dataframe(label="Historical Cards", interactive=False) | |
| hist_btn.click(fn=run_historical_scan, outputs=[hist_status, hist_results]) | |
| with gr.Tab("π° Price Check"): | |
| gr.Markdown("Check live market prices for all flagged targets. Alerts if price is below threshold.") | |
| gr.Markdown(f"*Thresholds: Legendary < {THRESHOLDS['legendary']}, Epic < {THRESHOLDS['epic']}, Ultra-Rare < {THRESHOLDS['ultra-rare']}*") | |
| max_price_input = gr.Number(label="Max cards to check:", value=30, minimum=1, maximum=200, precision=0) | |
| price_btn = gr.Button("π° Check Live Prices", variant="primary") | |
| price_alerts = gr.Markdown("") | |
| price_table = gr.Dataframe(label="Targets with Prices", interactive=False) | |
| price_btn.click(fn=run_price_check, inputs=[max_price_input], outputs=[price_alerts, price_table]) | |
| with gr.Tab("π All Targets"): | |
| gr.Markdown("View all currently flagged high-value targets.") | |
| refresh_btn = gr.Button("π Refresh") | |
| targets_table = gr.Dataframe(label="All Targets", interactive=False) | |
| refresh_btn.click(fn=get_all_targets, outputs=[targets_table]) | |
| with gr.Tab("π§ Smart Trading"): | |
| gr.Markdown("""### Smart Trading Advisor | |
| Analyzes targets using multiple factors to score buy opportunities (0-100): | |
| - **Price vs Market Value** β Is it undervalued? | |
| - **Price Trend** β Is the price dropping? | |
| - **Spread Analysis** β Room to negotiate? | |
| - **Rarity & Scarcity** β CTE/Historical bonuses | |
| - **Suggested Bids** β Optimal bid prices based on trade history | |
| """) | |
| smart_max = gr.Number(label="Max cards to analyze:", value=20, minimum=1, maximum=100, precision=0) | |
| gr.Markdown("*β±οΈ ~3.6s per card (3 API calls each)*") | |
| smart_btn = gr.Button("π§ Run Smart Analysis", variant="primary") | |
| smart_summary = gr.Markdown("") | |
| smart_table = gr.Dataframe(label="Trading Opportunities (sorted by score)", interactive=False) | |
| smart_btn.click(fn=run_smart_analysis, inputs=[smart_max], outputs=[smart_summary, smart_table]) | |
| with gr.Tab("π¬ Card Deep Dive"): | |
| gr.Markdown("Get detailed analysis of a specific card β market state, trade history, score, and bid suggestions.") | |
| dive_card_id = gr.Number(label="Card ID:", value=1, minimum=1, precision=0) | |
| dive_btn = gr.Button("π¬ Analyze Card", variant="primary") | |
| dive_report = gr.Markdown("") | |
| dive_trades = gr.Dataframe(label="Recent Trades", interactive=False) | |
| dive_btn.click(fn=run_card_deep_dive, inputs=[dive_card_id], outputs=[dive_report, dive_trades]) | |
| with gr.Tab("ποΈ Watchlist"): | |
| gr.Markdown("""### Personal Watchlist | |
| Add specific cards you want to buy. Set your max bid price and get alerted when asks drop to your target. | |
| """) | |
| with gr.Row(): | |
| wl_card_id = gr.Number(label="Card ID:", minimum=1, precision=0) | |
| wl_max_bid = gr.Number(label="Max bid price:", minimum=0, value=5.0) | |
| wl_notes = gr.Textbox(label="Notes:", placeholder="e.g. CTE legendary, want for collection") | |
| with gr.Row(): | |
| wl_add_btn = gr.Button("β Add to Watchlist", variant="primary") | |
| wl_remove_id = gr.Number(label="Remove Card ID:", minimum=1, precision=0) | |
| wl_remove_btn = gr.Button("ποΈ Remove", variant="secondary") | |
| wl_status = gr.Markdown("") | |
| wl_table = gr.Dataframe(label="Your Watchlist", interactive=False) | |
| wl_add_btn.click(fn=add_to_watchlist, inputs=[wl_card_id, wl_max_bid, wl_notes], outputs=[wl_status, wl_table]) | |
| wl_remove_btn.click(fn=remove_from_watchlist, inputs=[wl_remove_id], outputs=[wl_status, wl_table]) | |
| gr.Markdown("---") | |
| gr.Markdown("### Check Watchlist Prices") | |
| wl_check_btn = gr.Button("ποΈ Check Watchlist Alerts", variant="primary") | |
| wl_alerts = gr.Markdown("") | |
| wl_alert_table = gr.Dataframe(label="Watchlist Status", interactive=False) | |
| wl_check_btn.click(fn=check_watchlist_alerts, outputs=[wl_alerts, wl_alert_table]) | |
| wl_refresh = gr.Button("π Refresh Watchlist") | |
| wl_refresh.click(fn=get_watchlist, outputs=[wl_table]) | |
| with gr.Tab("βοΈ Config"): | |
| gr.Markdown(f""" | |
| ### Settings | |
| - **Season:** {SEASON} | |
| - **Rate Limit:** {RATE_LIMIT_SECONDS}s between API requests | |
| - **User-Agent:** `{USER_AGENT}` | |
| - **Max Scan ID:** 10,000 | |
| ### Price Alert Thresholds | |
| | Rarity | Alert if Ask Below | | |
| |---|---| | |
| | Legendary | {THRESHOLDS['legendary']} | | |
| | Epic | {THRESHOLDS['epic']} | | |
| | Ultra-Rare | {THRESHOLDS['ultra-rare']} | | |
| ### Smart Trading Score Factors | |
| | Factor | Max Points | Description | | |
| |---|---|---| | |
| | Price vs MV | 25 | Lower ask relative to market value = higher score | | |
| | Price Trend | 15 | Dropping prices = buy opportunity | | |
| | Spread | 10 | Larger ask-bid gap = negotiation room | | |
| | Rarity | 15 | Legendary > Epic > Ultra-Rare | | |
| | CTE Bonus | 10 | Dead nation = scarcer supply | | |
| | Historical | 5 | Famous NS names | | |
| ### Historical Names ({len(HISTORICAL_NAMES)} total) | |
| {', '.join(HISTORICAL_NAMES)} | |
| ### Compliance | |
| β οΈ This tool is **read-only**. No automated trades, buys, or bids are executed. | |
| All purchases must be completed manually via the generated URL links. | |
| """) | |
| app.launch() | |