Document generated from the analysis of 3 specialists (Architecture, Trading, Performance) on radar_poly.py.
Date: 2026-02-22 Branch: develop Last commit: 2147539
| Phase | Description | Status |
|---|---|---|
| 1 | Logging & Backtesting | ✅ Complete |
| 2 | New Indicators (MACD, VWAP, BB) | ✅ Complete |
| 3 | Market Regime Detection | ✅ Complete |
| 4 | Dynamic TP/SL & Trailing Stop | ⬜ Pending |
| 5 | Session Win Rate & Performance Stats | ✅ Complete |
| 6 | WebSocket Real-Time Data | ✅ Complete |
| 7 | Minor Improvements | ✅ Complete |
| 8 | Multi-Market Support | ✅ Complete |
- Problem:
monitor_tp_sl()is a blocking loop — while monitoring TP/SL, the radar stops updating, the static panel freezes, and the scrolling log receives no new data. - Solution: Integrate TP/SL checking into the main cycle (
while True). Each cycle (0.5s), check if price has reached TP or SL. Keep progress bar on the ACTION line of the static panel. - Impact: High — user experience and responsiveness
- Effort: Medium
- Files:
radar_poly.py— refactormonitor_tp_sl()to state-based instead of loop
- Problem: Fixed stop loss of
$0.06and TP based on fixed spread (0.05 + strength * 0.10). In a volatile market, SL is too tight (stopped out by noise); in a calm market, it's too wide (loses too much when wrong). - Solution: Use ATR (already available in
binance_data['atr']) to calculate dynamic TP/SL:TP = entry + ATR * 1.5SL = entry - ATR * 1.0- Minimum Risk:Reward ratio of 1.5:1
- Impact: High — risk management
- Effort: Low
- Files:
radar_poly.py— lines 308-311 (suggestiondict)
- Problem: During market transition (line 860-877), the code calculates theoretical P&L via
get_price(token_id, "SELL")but does not executeexecute_close_market(). The tokens remain in the user's wallet without being sold. - Solution: Call
execute_close_market()before calculating P&L on transition. If the sell fails, warn the user. - Impact: High — real money can get stuck in expired tokens
- Effort: Low
- Files:
radar_poly.py— market transition block (~line 860)
- Problem:
balanceis set once at startup (get_balance(client)) and then manually decremented/incremented on each trade. Over time, it diverges from the actual balance due to rounding, fees, and partially filled orders. - Solution: Call
get_balance(client)every 60s (along with the market refresh) to correct the drift. - Impact: Medium — incorrect information on the panel
- Effort: Low
- Files:
radar_poly.py— inside theif now - last_market_check > 60block - Status: ✅ Implemented —
get_balance(client)called every 60s in the market refresh block
- Problem: Once set, TP and SL are fixed. If the price goes 80% of the way to TP and then reverses, the trade can end at SL (-100% of risk).
- Solution: Trailing stop with 3 levels:
- Price reaches 50% of TP → move SL to breakeven (entry)
- Price reaches 75% of TP → move SL to 50% of profit
- Display updated trailing SL in the progress bar
- Impact: Medium — protects partial profit
- Effort: Medium
- Files:
radar_poly.py—monitor_tp_sl()(or new non-blocking logic)
- Problem:
get_price()creates a new HTTP connection on each call (requests.get()). With 2+ calls per cycle (UP + DOWN), that's ~4 new TCP connections per second in WebSocket mode. - Solution: Create a global or per-module
requests.Session()to reuse HTTP connections via keep-alive. - Impact: CRITICAL — each requests.get() opens a new TCP connection (+50-500ms per request)
- Effort: Low
- Files:
radar_poly.py(get_price()),binance_api.py,polymarket_api.py - Status: ✅ Implemented — global sessions in all 3 modules
- Problem:
ThreadPoolExecutor(max_workers=2)is created and destroyed each cycle (line 1149). In WebSocket mode (~2 cycles/s), that's 1800+ creations/destructions per hour. - Solution: Create the pool once at module level and reuse it.
- Impact: HIGH — thread creation/destruction overhead every 0.5-2s
- Effort: Low
- Files:
radar_poly.py— moveThreadPoolExecutorto module level - Status: ✅ Implemented — persistent pool with shutdown in finally
- Problem: The buy logic is duplicated in 3 locations:
- Lines 1075-1120 — signal buy (during opportunity, with TP/SL)
- Lines 1123-1134 — manual buy during opportunity (U/D)
- Lines 1159-1178 — manual buy during sleep cycle (U/D)
Each location repeats:
execute_hotkey()+last_action+positions.append()+balance -=+logger.log_trade(). Any change needs to be made in 3 places.
- Solution: Create
handle_buy(client, direction, amount, reason, ...)that encapsulates all buy logic. Same forhandle_close()for the 3 close locations (emergency, TP/SL, market expiry, exit). - Impact: Medium — reduces inconsistency bugs, eases maintenance
- Effort: Medium
- Files:
src/trade_executor.py—handle_buy(),close_all_positions(),execute_close_market() - Status: ✅ Implemented — extracted to
src/trade_executor.pywith dependency injection (get_price,executoras parameters)
- Problem:
monitor_tp_sl()runs indefinitely until TP, SL, or manual cancellation (C). If the price stays in the neutral zone for 15+ minutes, the market can change and the user is stuck in a loop without updates. - Solution: Add
timeout_sec(default 600s / 10min). If exceeded, return('TIMEOUT', current_price). - Impact: Low — edge case, but dangerous
- Effort: Low
- Files:
radar_poly.py—monitor_tp_sl()
- Problem: The entire system was hardcoded for
btc-updown-15m. 30+ references across 4 files. - Solution: New
market_config.pywithMarketConfigclass that centralizes configuration derived fromMARKET_ASSETandMARKET_WINDOW(.env). - Impact: High — enables operating BTC, ETH, SOL, XRP in 5min and 15min windows
- Effort: High
- Files:
market_config.py(new),polymarket_api.py,binance_api.py,ws_binance.py,radar_poly.py,.env.example - Status: ✅ Implemented — MarketConfig, find_current_market(config), symbol param in all Binance functions, dynamic WS, parameterized display, proportional phases
- Problem:
draw_panel()has 30 parameters. Hard to read, easy to get the order wrong, and any new data requires changing the signature + all 4 call sites. - Solution: Create
@dataclass PanelStatewith all fields. Pass a single object todraw_panel(). - Impact: Low — readability and maintainability
- Effort: Medium
- Files:
src/ui_panel.py
- Problem: The scrolling line formatting (lines 966-1054) is ~90 lines of column construction inside the main loop.
- Solution: Extract to
format_scrolling_line(signal, btc_price, up_buy, down_buy, positions, regime). - Impact: Low — readability
- Effort: Low
- Files:
src/ui_panel.py - Status: ✅ Implemented — extracted to
src/ui_panel.py
- Problem:
history = deque(maxlen=60)is global and_ema()has no protection against timestamp discontinuities. If there's a data gap (WS disconnects for 5min), the EMA mixes old data with new. - Solution: Before calculating EMA, filter
histremoving entries with timestamp > 30s difference from the previous one. - Impact: Low — edge case of WS reconnection
- Effort: Low
- Files:
radar_poly.py—compute_signal()
- Problem:
binance_api.pyandpolymarket_api.pycreate individual HTTP connections per request. - Solution: Use
requests.Session()in each module to reuse connections. - Impact: Low — complements item 6
- Effort: Low
- Files:
binance_api.py,polymarket_api.py - Status: ✅ Implemented along with item 6
- Problem: When price crosses VWAP upward but fails to hold, it's a strong DOWN signal. Currently not detected.
- Solution: Track VWAP crossovers. If price crossed above in the last 5 samples but is now below → boost DOWN signal.
- Impact: Low — incremental signal improvement
- Effort: Low
- Files:
radar_poly.py—compute_signal()
- Problem: When the 15min window closes, the radar waits until the next 60s check to find the new market.
- Solution: When
time_remaining < 0.5min, check every 10s instead of 60s. Display switching notification. - Impact: Low — reduces gap between markets
- Effort: Low
- Files:
radar_poly.py— market refresh logic
- Problem: No audio alert for the highest-probability trading setup (mean reversion at RSI extremes + Bollinger touch).
- Solution: Added Mean Reversion Alert that beeps when: MID phase + RSI ≤ 15 or ≥ 85 + BB ≤ 0.10 or ≥ 0.90 + reversal-side token < $0.70. This is now the only beep in the system (all other beeps disabled by default).
- Impact: High — guides trader to highest-probability entries
- Files:
radar_poly.py— main loop alert block - Status: ✅ Implemented
- Problem: After opening a position, no audio feedback when TP or SL levels are reached.
- Solution: Continuous monitoring of all open positions. TP = entry + $0.20 (cap $0.55), SL = entry - $0.15 (floor $0.05). Beeps on hit (2 beeps for TP, 1 for SL) with 15s cooldown.
- Impact: High — trader can focus on other tasks while monitoring runs
- Files:
radar_poly.py— main loop position monitor block - Status: ✅ Implemented
- Problem: Default indicator periods and weights were not optimized for 15-minute mean reversion strategy.
- Solution: Updated defaults based on research — RSI=5 (from 7), MACD=12/26/9 (from 5/10/4), BB=10/1.5 (from 14/2.0), ADX=14 (from 7). Weights rebalanced: divergence 25% (from 20%), bollinger 15% (from 10%), MACD 10% (from 15%).
- Impact: High — better signal quality for mean reversion
- Files:
.env.example,src/binance_api.py(BB_STD int→float fix) - Status: ✅ Implemented
- Problem: No way to disable individual alert types (price alert, signal opportunity) without modifying code.
- Solution: Added
PRICE_ALERT_ENABLED,SIGNAL_ENABLED, andPRICE_BEAT_ALERTvariables to.env. Default disabled in.envfor users who prefer mean reversion only. - Impact: Medium — flexibility for different trading styles
- Files:
radar_poly.py,.env.example - Status: ✅ Implemented
- Problem: Positions opened/closed directly on the Polymarket website were not reflected in the radar.
- Solution: Every 60 seconds, sync positions from the platform via API. Detects new positions opened outside the script and positions closed on the website. Balance also re-synced.
- Impact: High — seamless integration with web UI
- Files:
radar_poly.py,src/polymarket_api.py - Status: ✅ Implemented
The main() function (lines 717→1317) concentrates all system logic: UI, trading, hotkeys, market transition, alerts and session. It is the main maintainability bottleneck.
Status: Partially resolved. radar_poly.py was refactored from 1,591 → 712 lines (-55%). Six modules were extracted to src/, a TradingSession class encapsulates all mutable state, and exponential backoff was added on API failures. The main() function still contains the event loop (~350 lines) but now delegates to extracted modules for all heavy logic.
| Lines | Block | Lines | Extractable? |
|---|---|---|---|
| 717-723 | Parse trade_amount argument | 6 | Yes |
| 725-749 | Logger + donation banner + countdown | 24 | Partial |
| 751-770 | Connect API + balance + find market | 19 | Partial |
| 772-780 | Price to Beat (historical BTC) | 8 | Yes |
| 782-800 | Start WebSocket | 18 | Yes |
| 802-820 | Configure terminal (tty, scroll region) | 18 | Yes |
| 822-842 | Initialize session variables + initial panel | 20 | Partial |
| 846-928 | Data collection (WS/HTTP candles, analysis) | 82 | Yes |
| 930-947 | Fetch token prices + compute signal | 17 | Partial |
| 949-964 | Log signal + update static panel | 15 | Partial |
| 967-1055 | Format scrolling line (columns + colors) | 88 | Yes |
| 1057-1138 | Opportunity + hotkeys (signal buy + TP/SL) | 81 | Yes |
| 1140-1154 | Price alert (beep) | 14 | Yes |
| 1156-1220 | Sleep + manual hotkeys (U/D/C/Q) | 64 | Partial |
| 1222-1240 | KeyboardInterrupt: close positions | 18 | Partial |
| 1242-1297 | Session summary (calculation + print + log) | 55 | Yes |
| 1305-1312 | Finally: cleanup (WS stop, terminal restore) | 7 | Partial |
Pattern A — Close positions (3 copies)
Appears in: market transition (L863), emergency close (L1194), exit (L1231)
for p in positions:
token_id = token_up if p['direction'] == 'up' else token_down
price = get_price(token_id, "SELL")
pnl = (price - p['price']) * p['shares']
session_pnl += pnl
trade_count += 1
trade_history.append(pnl)
logger.log_trade("CLOSE", p['direction'], p['shares'], price, ...)
balance += price * p['shares']
positions.clear()Solution: close_all_positions(positions, token_up, token_down, logger, reason) → returns (total_pnl, count, pnl_list)
Pattern B — Execute buy (3 copies)
Appears in: signal buy (L1077), manual during opportunity (L1125), manual during sleep (L1160)
info = execute_hotkey(client, direction, trade_amount, token_up, token_down)
if info:
positions.append(info)
balance -= info['price'] * info['shares']
logger.log_trade("BUY", direction, info['shares'], info['price'], ...)
last_action = f"BUY {direction.upper()} ...sh @ $..."
else:
last_action = f"✗ BUY {direction.upper()} FAILED"Solution: handle_buy(client, direction, amount, reason, ...) → returns (info, last_action)
Pattern C — Call draw_panel (4 copies)
Appears in: initial panel (L838), after signal (L956), emergency close before (L1183), emergency close after (L1209)
All with ~12 identical repeated parameters.
Solution: PanelState dataclass — update individual fields, pass single object
Session (defined once):
trade_amount, logger, session_start, session_start_str, trade_history, client, limit, balance, event, market, token_up, token_down, time_remaining, market_slug, price_to_beat, binance_ws, ws_started, old_settings, fd, is_tty
Loop state (mutable each cycle):
last_beep, last_market_check, base_time, positions, current_signal, alert_active, alert_side, alert_price, session_pnl, trade_count, status_msg, status_clear_at, last_action, now, now_str
Temporary within loop (~30+):
ws_candles, data_source, bin_direction, confidence, details, btc_price, binance_data, current_regime, up_buy, down_buy, s_dir, strength, rsi_val, trend, sr_raw, sr_adj, col_* (15 column variables), blocks, bar, color, sym, ...
| Function to extract | Eliminates | Lines saved | Status |
|---|---|---|---|
close_all_positions() |
Pattern A (3x) | ~40 | ✅ src/trade_executor.py |
handle_buy() |
Pattern B (3x) | ~50 | ✅ src/trade_executor.py |
format_scrolling_line() |
Block 967-1055 | ~88 | ✅ src/ui_panel.py |
calculate_session_stats() |
Block 1242-1262 | ~30 | ✅ src/session_stats.py |
compute_signal() |
Signal engine | ~170 | ✅ src/signal_engine.py |
draw_panel() |
Static panel UI | ~180 | ✅ src/ui_panel.py |
detect_scenario() |
Scenario detection | ~55 | ✅ src/signal_engine.py |
execute_buy/close/monitor |
Trade execution | ~235 | ✅ src/trade_executor.py |
read_key/wait/sleep |
Input handling | ~55 | ✅ src/input_handler.py |
TradingSession class |
17 local vars → class | — | ✅ radar_poly.py |
PanelState dataclass |
Pattern C (4x) | ~20 | ⬜ Pending |
Result: radar_poly.py dropped from 1,591 → 712 lines (-55%). TradingSession class encapsulates all mutable state. main() now delegates to 6 extracted modules.
| # | Problem | Severity | Location | Impact |
|---|---|---|---|---|
| 1 | No requests.Session() |
CRITICAL | binance_api.py, polymarket_api.py, radar_poly.py | +50-500ms per request (new TCP connection) |
| 2 | get_price() without cache |
CRITICAL | radar_poly.py:117 | 2-10 duplicated requests/s in monitor_tp_sl() |
| 3 | ThreadPoolExecutor recreated each cycle | HIGH | radar_poly.py:1149 | 1800+ creations/hour |
| 4 | monitor_tp_sl() blocking I/O |
HIGH | radar_poly.py:885 | Response time 1+s |
| 5 | 65+ sys.stdout.write() per redraw | MEDIUM | radar_poly.py:546-721 | 2200+ terminal ops/min |
| 6 | HTTP polling with active WebSocket | MEDIUM | binance_api.py:411 | Unnecessary HTTP for indicators |
| 7 | Unnecessary deque/list copies | LOW | radar_poly.py:165, ws_binance.py:108 | Minimal CPU/mem |
1. HTTP Connection Reuse (CRITICAL)
- Each
requests.get()without Session opens a new TCP connection: handshake 50-100ms on fast network, 200-500ms on slow network get_price()called 2x per cycle (UP + DOWN), 0.5s cycle = 4 connections/secondcheck_limit()makes 2 sequential requests in polymarket_api.py:254-269- Fix:
session = requests.Session()at module level, replacerequests.get()→session.get()
2. get_price() Without Cache (CRITICAL)
- Called in: close_all_positions (per position), execute_buy_market (entry), execute_close_market (3x retry), monitor_tp_sl (every 0.5s), main loop (UP+DOWN)
- In
monitor_tp_sl(): polling every 0.5s, each HTTP takes 100-500ms - Fix: PriceCache with 0.5s TTL to avoid duplicates within the same cycle
3. ThreadPoolExecutor (HIGH)
- Line 1149:
with ThreadPoolExecutor(max_workers=2) as pool:inside the loop - Pool creation includes: thread allocation, state initialization, locks
- With 0.5s cycle: ~7200 creations/destructions per hour
- Fix: Persistent pool at module level,
executor.shutdown()in finally
4. monitor_tp_sl() Blocking (HIGH)
get_price()blocks for 100-500ms → actual cycle is 1+s instead of 0.5s- Key checking happens AFTER the fetch (not concurrent)
- Fix: Asynchronous fetch + concurrent key checking via ThreadPoolExecutor
5. Terminal Rendering (MEDIUM)
draw_panel()makes 66+ individualsys.stdout.write()calls per redraw- Each
flush()triggers I/O to the terminal - 33+ redraws/min × 66 ops = 2200+ terminal operations/minute
- Fix: Accumulate in
io.StringIO(), singlewrite()+flush()
| # | Fix | Status |
|---|---|---|
| 1 | Persistent requests.Session() | ✅ Implemented |
| 2 | PriceCache with TTL | ✅ Implemented |
| 3 | Persistent ThreadPoolExecutor | ✅ Implemented |
| 4 | Concurrent monitor_tp_sl() | ✅ Implemented |
| 5 | Batch terminal writes (StringIO) | ✅ Implemented |
| 6 | Optimize polling with active WS | ⬜ Pending (requires further analysis) |
| 7 | Eliminate deque copies | ✅ Implemented |
Combined estimate: 60-70% reduction in network latency, 40-50% reduction in CPU overhead.
- ✅ Color on scrolling column RSI (green < 40, red > 60)
- ✅ Color on scrolling strength bar
- ✅ Color on SIGNAL line indicators (RSI, Trend, MACD, VWAP, BB)
- ✅ ALERT color (green UP, red DOWN)
- ✅ S/R color (green positive, red negative)
- ✅ BB color fixed (green > 80%, red < 20%)
- ✅ BB column alignment (fixed width 6 chars)
- ✅ RSI header alignment (7 chars)
- ✅ RG column renamed to REGIME
- ✅ ACTION line on static panel (trades without polluting scrolling)
- ✅ Silent
execute_hotkey()(quiet=True) - ✅ C key works during TP/SL monitoring
- ✅ Market transition calculates real P&L (fetch SELL price)
- ✅ Session summary on exit (clear + print)
- ✅ Virtual environment not activated warning
- ✅ WS auto-recovery in main loop
- ✅ WebSocket instead of WS on BINANCE line
- ✅ Donation banner with 20s countdown
- ✅ WR/PF displayed on POSITION line
- Dynamic TP/SL with ATR (#2)
- TP/SL non-blocking (#1)
- Trailing stop (#5)
- Timeout on monitor_tp_sl (#9)
- Market expiry real close (#3)
- Re-sync balance (#4) ✅
- Persistent requests.Session() (#6) ✅
- Persistent ThreadPoolExecutor (#7) ✅
- Extract handle_buy/handle_close (#8) ✅
- Extract format_scrolling_line (#12) ✅
- PanelState dataclass (#11) — ⬜ Pending
- TradingSession class ✅ — encapsulates 17 mutable state variables
- Module extraction ✅ — 6 new modules: signal_engine, ui_panel, trade_executor, input_handler, session_stats, colors
- Exponential backoff on API failures ✅ — Binance errors, market refresh, market slug lookup
- MarketConfig + parameterization (#10) ✅
- History gap protection (#13)
- Connection pooling (#14)
- VWAP reclaim detection (#15)
- Market transition handling (#16)
Date: 2026-02-22 Context: Deep analysis of the system from the perspective of a specialist trader in updown crypto markets on Polymarket. Focus on problems that directly affect P&L.
- Problem: The Price to Beat (BTC price at the start of the window) is the most important metric in an updown market — it defines the outcome (BTC above = UP wins, below = DOWN wins). However
compute_signal()completely ignores this information. The signal uses generic RSI/MACD/VWAP without considering the reference that defines the market outcome. - Example: If BTC is $500 above the beat price with 3 minutes remaining, the probability of UP is very high. But the signal might say DOWN if RSI is overbought and MACD crosses downward — a severe false negative.
- Solution: New component
beat_distance_score:Dynamic weight by phase:beat_diff_pct = (btc_price - price_to_beat) / price_to_beat * 100 # Scale: each 0.1% difference = ~10 confidence points beat_score = max(-1.0, min(1.0, beat_diff_pct / 0.3))
- EARLY: 10% (BTC can reverse, low predictive value)
- MID: 25% (trend is established, moderate weight)
- LATE: 50% (almost deterministic, dominates the signal)
- Impact: HIGH — this is the most predictive information for the final outcome
- Effort: Low
- Files:
radar_poly.py—compute_signal()receivesprice_to_beatandphaseas parameters
- Problem: When UP=$0.92, buying UP yields at most $0.08 (8.7% upside) but can lose $0.91 (98.9% downside). The risk/reward is 1:11 — catastrophic. The script does not prevent this buy. Similarly, buying DOWN at $0.05 has $0.94 upside but very low probability.
- Concrete example: Trader sees UP 75% signal, presses S. The token is at $0.93. Buys 43 shares at $0.93 = $40. If it resolves UP, gains $3. If it resolves DOWN, loses $40.
- Solution: Block or alert when the entry token exceeds threshold:
When blocked, show in ALERT:
MAX_ENTRY_PRICE = 0.85 # Don't buy above 85 cents MIN_ENTRY_PRICE = 0.08 # Don't buy below 8 cents (very low probability)
BLOCKED: UP@$0.93 — risk/reward 1:11 (max $0.07 / risk $0.93) - Impact: HIGH — prevents the worst possible losses (near-total trade loss)
- Effort: Low (5-10 lines)
- Files:
radar_poly.py— inside the detected opportunity block and inhandle_buy()
- Problem: The
check_limit()function exists inpolymarket_api.py:246and calculates total exposure (positions + open orders) vs POSITION_LIMIT. But it is never called in the trade flow:handle_buy()→execute_hotkey()→execute_buy_market()proceeds without checking. The user can accumulate unlimited exposure. - Example: POSITION_LIMIT=76 in .env. Trader buys 10x of $10 = $100 exposure. No blocking.
- Solution: Call
check_limit()at the beginning ofhandle_buy():can_trade, exposure, limit = check_limit(client, token_up, token_down, trade_amount) if not can_trade: last_action = f"BLOCKED: exposure ${exposure:.0f}/${limit:.0f}" return None, balance, last_action
- Impact: HIGH — fundamental risk control that already exists but is not connected
- Effort: Low (10 lines)
- Files:
radar_poly.py—handle_buy()+ passclient, token_up, token_downas args
- Problem: Market refresh happens every 60s (
last_market_check). If the last check was at T-80s, positions might not close before market resolution. Updown tokens resolve automatically: those who were right receive $1, those who were wrong receive $0. If the trader has the right position, they might miss the opportunity to sell at $0.95 before resolution (since the market resolves at $1.00, but without liquidity in the last seconds). - Real risk: If on the wrong side, the token goes to $0.00 — total loss. And in the last 30-60 seconds, the orderbook spread widens significantly, making the close difficult.
- Solution: Hard cutoff at T-45s (configurable via .env
AUTO_CLOSE_SECONDS=45):if current_time <= 0.75 and positions: # 45 seconds status_msg = "AUTO-CLOSE: market expiring" execute_close_market(client, token_up, token_down) close_all_positions(...)
- Impact: HIGH — prevents total loss from resolution on the wrong side
- Effort: Low (15 lines)
- Files:
radar_poly.py— in main loop, before the data collection block
- Problem: Current TP/SL is fixed:
spread = 0.05 + (strength/100)*0.10,sl = entry - 0.06. This does not consider:- Entry price: SL of $0.06 on a $0.90 token is 6.6% risk, but on a $0.20 token is 30%.
- Time remaining: With 10min remaining, TP of +$0.15 is achievable. With 1min, it's impossible.
- Volatility: In a volatile market, tight SL = stopped out by noise. In a calm market, wide SL = unnecessary loss.
- Solution: Adaptive TP/SL based on ATR + time + entry:
atr = binance_data.get('atr', 0) atr_pct = atr / btc_price if btc_price > 0 else 0.001 time_factor = min(current_time / 10, 1.0) # decreases with time # Based on ATR, adjusted by time remaining tp_spread = max(0.03, atr_pct * 50 * time_factor) sl_spread = max(0.02, atr_pct * 30 * time_factor) # Limit by entry price (SL cannot exceed 50% of entry) sl_spread = min(sl_spread, entry * 0.20) tp = min(entry + tp_spread, 0.95) sl = max(entry - sl_spread, 0.03)
- Impact: HIGH — TP/SL responsive to actual market conditions
- Effort: Medium
- Files:
radar_poly.py—compute_signal()(suggestion) +monitor_tp_sl()
- Additional trading problem: While
monitor_tp_sl()blocks, the market can change windows (15min have passed), the regime can switch from TREND to CHOP, and new opportunities are missed. Worse: if the market expires during monitoring, the P&L may not be calculated correctly. - Trading solution: In the main loop, maintain
active_tp_sl = {'token_id': ..., 'tp': ..., 'sl': ..., 'entry': ...}. Each cycle:- Fetch price of the monitored token
- Check TP/SL/Trailing
- Display progress bar on ACTION line
- Allow hotkey C to cancel
- If TP/SL hit, execute close automatically
- Impact: HIGH — trader maintains full market view during open position
- Effort: Medium-High
- Files:
radar_poly.py— replacemonitor_tp_sl()with state machine in loop
- Problem: If the trader loses 5 consecutive trades ($30 loss on a $76 account), the system continues operating normally. Without a per-session loss limit, a bad day can decimate the entire account.
- Solution: New .env variable
MAX_SESSION_LOSS=20(default $20). Whensession_pnl <= -MAX_SESSION_LOSS:- Automatically close all positions
- Disable trading (ignore hotkeys U/D/S)
- Show on STATUS line:
CIRCUIT BREAKER: session loss $-20.00 (limit: $20) - Continue displaying the radar (monitoring) but without executing trades
- Hotkey R to reset the breaker (requires confirmation)
- Impact: MEDIUM-HIGH — essential protection against bad days
- Effort: Low (20 lines)
- Files:
radar_poly.py— check inhandle_buy()+ new .env variable
- Problem: After a loss, the trader (and the system) can immediately enter a new trade. In real trading, this leads to "revenge trading" — emotional trades that accumulate more losses. The signal may be correct but the market condition that caused the loss still persists.
- Solution: Configurable cooldown after loss:
COOLDOWN_AFTER_LOSS=30(seconds). After closing a position with negative P&L:During cooldown:if pnl < 0: cooldown_until = time.time() + COOLDOWN_AFTER_LOSS
- Signals continue to be calculated and displayed
- Opportunities are detected but do not offer prompt (S/U/D ignored)
- STATUS line shows:
COOLDOWN: 25s remaining (last trade: -$1.50)
- Impact: MEDIUM — prevents accumulation of consecutive losses
- Effort: Low (15 lines)
- Files:
radar_poly.py— variablecooldown_until, check in the opportunity block
- Problem: The 6 signal weights (Momentum 30%, Divergence 20%, S/R 10%, MACD 15%, VWAP 15%, BB 10%) are fixed. But the usefulness of each indicator changes drastically throughout the window:
- EARLY (>66%): Momentum/MACD are informative (trend forming), but Price-to-Beat has low predictive value (BTC can reverse multiple times)
- MID (33-66%): All indicators have similar value
- LATE (<33%): The BTC vs Beat distance is almost deterministic. RSI oversold is irrelevant if BTC is $300 above beat with 2 minutes remaining
- Solution: Weight table by phase:
Component EARLY MID LATE ───────────────────────────────────── Beat Distance 10% 25% 50% Momentum 30% 20% 10% Divergence 20% 15% 5% MACD 15% 15% 10% VWAP 15% 15% 10% S/R 5% 5% 10% Bollinger 5% 5% 5% - Impact: MEDIUM — better calibrated signal for each moment of the window
- Effort: Medium (weight table + refactor compute_signal)
- Files:
radar_poly.py—compute_signal()receivesphaseand adjusts weights
- Problem: Several indicators use thresholds in absolute dollar values:
- MACD:
abs(macd_hist_delta) > 0.5(line 266) — $0.50 in BTC at $98k is 0.0005%, irrelevant. At $20k it would be 0.0025%, more significant. - VWAP:
vwap_pos > 0.02(line 283) — 0.02% is ~$20 in BTC at $98k. Too small to be significant. - ATR-based vol threshold:
VOL_THRESHOLD=0.03(3%) is reasonable but static.
- MACD:
- Solution: Normalize by current price. For MACD:
For VWAP: increase threshold to
macd_delta_pct = macd_hist_delta / btc_price * 100 # in percentage if abs(macd_delta_pct) > 0.0005: macd_score = 1.0 if macd_delta_pct > 0 else -1.0
0.05(0.05% = ~$50 in BTC at $98k). - Impact: MEDIUM — reduces false signals from miscalibrated thresholds
- Effort: Low (adjust 3-4 comparisons)
- Files:
radar_poly.py—compute_signal()components 4 (MACD) and 5 (VWAP)
- Problem:
positions = []is initialized empty at the start of main(). If the script crashes and restarts, it doesn't know about existing positions in the UP/DOWN tokens. The trader may have shares that don't appear on the panel, and the session P&L starts wrong. - Solution:
sync_positions()insrc/trade_executor.pyqueriesget_token_position()for both UP/DOWN tokens, compares with local tracking, and adds/removes positions accordingly. - Impact: MEDIUM — resilience to crashes + correct information on panel
- Effort: Low
- Files:
src/trade_executor.py—sync_positions(),radar_poly.py— startup + periodic sync - Status: ✅ Implemented —
sync_positions()called on startup (detects existing positions) and every 60s in the market refresh block (detects buys/sells made directly on Polymarket's web interface). Balance also re-synced viaget_balance()on each refresh.
- Problem: The Divergence component (BTC vs Polymarket price) looks at only 6 past cycles (~12 seconds with 2s cycle, ~3s with WS). In crypto, 12-second movements are pure noise — any micro-fluctuation generates false "divergence".
- Solution: Increase lookback to 30-60 cycles (~1-2 minutes). With more history, the detected divergence is more significant:
Additionally, consider using the average of the last 5 old points vs last 5 recent points to smooth noise.
DIVERGENCE_LOOKBACK = 30 # cycles (~60s with 2s cycle, ~15s with WS 0.5s) if len(history) >= DIVERGENCE_LOOKBACK: h_old = history[-DIVERGENCE_LOOKBACK] h_new = history[-1] ...
- Impact: MEDIUM — reduces false divergences
- Effort: Low (change 1 constant + optional smoothing)
- Files:
radar_poly.py—compute_signal()component 2 (Divergence)
- Problem: The current S/R component calculates support/resistance on UP token prices (values between $0.01-$0.99). This reflects what the market has already priced in, not what will happen. The true driver is the BTC price — if BTC is testing support at $68,000, that's new information that the Polymarket market may not have priced in yet.
- Solution: Calculate S/R using BTC prices from Binance candles:
Bonus: detect round numbers ($68,000, $68,500, etc.) as psychological support/resistance:
btc_prices = [c['high'] for c in candles[-20:]] + [c['low'] for c in candles[-20:]] btc_high = max(btc_prices) btc_low = min(btc_prices) btc_range = btc_high - btc_low # Current price position in the recent range if btc_range > 0: btc_pos = (btc_price - btc_low) / btc_range if btc_pos < 0.15: sr_score = 0.8 # near support = UP elif btc_pos > 0.85: sr_score = -0.8 # near resistance = DOWN
round_500 = round(btc_price / 500) * 500 dist_to_round = abs(btc_price - round_500) / btc_price if dist_to_round < 0.001: # within 0.1% of a round number sr_score *= 1.2 # amplify S/R signal
- Impact: MEDIUM — S/R on BTC is more predictive than on token price
- Effort: Medium (rewrite S/R component)
- Files:
radar_poly.py—compute_signal()component 3, orbinance_api.pynew functioncompute_sr_levels(candles)
- Problem: Binance volume is calculated (
vol_up,vol_downinanalyze_trend()) but only used for display (HIGH/normal flag). A bullish signal without buying volume is much less reliable — it may just be a fluctuation due to lack of liquidity. - Solution: Use volume ratio as multiplier of the final score:
vol_ratio = vol_up / (vol_up + vol_down) if (vol_up + vol_down) > 0 else 0.5 # If volume confirms direction, boost. If contradicts, dampen. if score > 0 and vol_ratio > 0.60: score *= 1.15 # volume confirms bullish elif score > 0 and vol_ratio < 0.40: score *= 0.75 # volume contradicts bullish elif score < 0 and vol_ratio < 0.40: score *= 1.15 # volume confirms bearish elif score < 0 and vol_ratio > 0.60: score *= 0.75 # volume contradicts bearish
- Impact: LOW-MEDIUM — incremental false signal filter
- Effort: Low (10 lines)
- Files:
radar_poly.py—compute_signal()after final score, before regime adjustment
- Problem: The script fetches only best BUY and best SELL price. It doesn't show the spread (difference between bid and ask). Wide spread = low liquidity = higher entry/exit cost = higher effective slippage. On tokens with $0.10 spread, a $4 trade already has $0.40 implicit cost (10%).
- Solution: Calculate and display spread on the POLY line:
Additionally, use spread as filter: if
up_sell = get_price(token_up, "SELL") spread_up = up_buy - up_sell # POLY │ UP: $0.55/$0.45 (55%) spread:$0.03 │ DOWN: ...
spread > 0.08, alert that transaction cost is high. - Impact: LOW — useful information for manual decision, liquidity filter
- Effort: Medium (additional fetch + display + filter)
- Files:
radar_poly.py—draw_panel()POLY line,compute_signal()as filter
- Enforce check_limit() (T3) — Low effort, immediate impact
- Risk/reward extremes filter (T2) — Low effort, prevents worst losses
- Auto-close before expiration (T4) — Low effort, prevents total loss
- Session max-loss circuit breaker (T7) — Low effort, session protection
- Price-to-Beat in signal (T1) — The most predictive information for the outcome
- Proportional TP/SL (T5) — Adaptive risk management
- Post-loss cooldown (T8) — Prevent revenge trading
- Dynamic weights by phase (T9) — Signal adapts to temporal window
- Normalize thresholds (T10) — Reduces false signals
- Longer Divergence lookback (T12) — More significant divergence
- Non-blocking TP/SL (T6) — Full view during position
- Recover positions on startup (T11) — Resilience ✅
- S/R at BTC levels (T13) — More predictive S/R
- Volume as multiplier (T14) — Signal confirmation
- Spread monitoring (T15) — Liquidity information
Date: 2026-02-22 Branch: feature/melhores_praticas Scope: All 6 project files
- Problem: Exceptions are silently swallowed in
radar_poly.py(lines 147, 370, 393, 955, 1241, 1273),polymarket_api.py(lines 60, 114, 241),ws_binance.py(line 233),logger.py(lines 120, 137, 164). Network, parsing and API errors go unnoticed — impossible to diagnose failures. - Solution: Replace with specific exceptions (
requests.RequestException,ValueError,KeyError,json.JSONDecodeError) with logging vialoggingmodule. - Files: All 6 files
- Status: ⬜ Pending
- Problem:
POLYMARKET_API_KEYis in.envwithout rotation or encryption. If.envleaks, funds are compromised. - Solution: Verify
.gitignoreincludes.env, add warning in.env.example, document use of secrets manager for production. - Files:
polymarket_api.py,.env.example - Status: ⬜ Pending (mitigation —
.envis already in.gitignore)
- Problem:
PriceCache.get()returns 0.0 on network failure and caches for 0.5s. Delays real price detection and can generate incorrect decisions. - Solution: Don't cache when response is an error. Distinguish real 0.0 from failure.
- Files:
radar_poly.py— classPriceCache - Status: ⬜ Pending
- Problem: During cleanup, trades in execution may be cancelled mid-operation.
- Solution: Use
wait=Trueor ensure futures complete before shutdown. - Files:
radar_poly.py— finally block - Status: ⬜ Pending
- Problem:
get_full_analysis()is a blocking HTTP call on the main thread. If API takes 5-10s, the entire UI freezes. - Solution: Move to executor with timeout.
- Files:
radar_poly.py— main loop - Status: ⬜ Pending
- Problem:
ws_binance.pychecks_thread.is_alive()and creates new thread outside the lock. Thread can die between check and start. - Solution: Protect with lock.
- Files:
ws_binance.py - Status: ⬜ Pending
- Problem:
get_balance(),get_open_orders_value(),get_price_at_timestamp()silently swallow exceptions. - Solution: Specific catch + logging.
- Files:
polymarket_api.py - Status: ⬜ Pending
- Problem: Impossible to unit test. Mixes data fetching, signal computation, UI, trade execution, hotkeys.
- Solution: Extract
fetch_market_data(),process_signal(),handle_input(),update_ui(). - Files:
radar_poly.py,src/signal_engine.py,src/ui_panel.py,src/trade_executor.py,src/input_handler.py,src/session_stats.py - Status: ✅ Implemented — 6 modules extracted,
TradingSessionclass added,radar_poly.pyreduced from 1,591 to 712 lines
- Problem:
history.append()inside a function that should be pure. Hidden side-effect makes function untestable. - Solution: Return data for append externally, or move append to the caller.
- Files:
src/signal_engine.py - Status: ✅ Implemented —
historyis now a parameter ofcompute_signal(), not a global. Append happens inmain()before calling the function.
- Problem: Values like
0.02,0.05,0.06,5,30,999,0.99scattered without explanation. - Solution: Create constants with descriptive names at the top of the file.
- Files:
src/signal_engine.py,src/trade_executor.py - Status: ✅ Implemented — all magic numbers extracted as named constants in their respective modules
- Problem:
time_remaining / window_min(line 178),gw / gl(line 720) can cause crash. - Solution: Add guards
if x > 0or use safe division. - Files:
radar_poly.py - Status: ⬜ Pending
- Problem: All main functions without type annotations. IDE and analysis tools cannot validate.
- Solution: Add type hints for parameters and returns.
- Files: All 6 files
- Status: ⬜ Pending
- Problem: In 5+ functions, docstring appears after code (if/validation) instead of at the beginning of the function.
- Solution: Move docstrings to right after
def. - Files:
binance_api.py - Status: ⬜ Pending
- Problem:
datetime.now()without timezone on line 153. If server changes TZ, breaks silently. - Solution: Use
datetime.now(BRASILIA)explicitly. - Files:
polymarket_api.py - Status: ⬜ Pending
- Problem: 4 slug attempts without delay between requests. Can cause rate limiting.
- Solution: Add increasing sleep between attempts.
- Files:
src/polymarket_api.py - Status: ✅ Implemented — exponential backoff:
0.2 * 2^i(0.2s, 0.4s, 0.8s, 1.6s), capped at 2.0s
- Problem: Files opened with
open()withoutwithstatement. If exception occurs, file descriptor leaks. - Solution: Refactor to use context managers or ensure close in finally.
- Files:
logger.py - Status: ⬜ Pending
- Problem: Position aggregation appears in
format_scrolling_line()anddraw_panel(). - Solution: Extract to
aggregate_positions()function. - Files:
src/ui_panel.py - Status: ⬜ Pending
- Problem:
get_candles()reads_currentoutside the lock in some paths. - Solution: Always read candle state inside the lock.
- Files:
ws_binance.py - Status: ⬜ Pending
- Problem: Allocation every 0.5-2s cycle.
- Solution: Use
itertools.isliceor iterate directly. - Files:
radar_poly.py - Status: ⬜ Pending
- Problem: Syscall on every panel redraw.
- Solution: Cache and update with SIGWINCH.
- Files:
radar_poly.py - Status: ⬜ Pending
- Problem:
print()for errors mixes with UI output. Impossible to filter by severity. - Solution: Use
logging.warning(),logging.error()for internal errors. - Files:
radar_poly.py - Status: ⬜ Pending
- Problem: Inner function recreated on each invocation.
- Solution: Move to module level.
- Files:
binance_api.py - Status: ⬜ Pending
- Problem:
from market_config import MarketConfiginside function (line 146). - Solution: Move to top of file.
- Files:
polymarket_api.py - Status: ⬜ Pending
- Replace bare excepts with specific exceptions (P1)
- Don't cache errors in PriceCache (P3)
- Fix silent exceptions in polymarket_api/ws_binance (P6, P7)
- Protect divisions by zero (P11)
- Correct ThreadPoolExecutor shutdown (P4)
- Blocking I/O with timeout on main thread (P5)
- Race condition on WS restart (P6)
- Thread safety on
_current(P18)
- Magic numbers → named constants (P10) ✅
- compute_signal() without side effects (P9) ✅
- Type hints on public functions (P12)
- Correct docstrings in binance_api.py (P13)
- Explicit timezone (P14)
- Retry with backoff (P15) ✅
- Logger file handles (P16)
- Performance: cache terminal size, deque slicing (P19, P20)
- Circular import (P23)