Download src/ui/testnet_client.py from chen470/drl-trading-bot-dev2: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/spaces/chen470/drl-trading-bot-dev2/resolve/main/src/ui/testnet_client.py
- Command line
-
hf download hf://spaces/chen470/drl-trading-bot-dev2/src/ui/testnet_client.py
-
curl -L -o testnet_client.py https://huggingface.co/spaces/chen470/drl-trading-bot-dev2/resolve/main/src/ui/testnet_client.py
14.1 kB
| """ | |
| Client-side Binance Testnet Integration | |
| Makes API calls from user's browser to bypass HuggingFace geo-restrictions | |
| """ | |
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| def render_testnet_tab(api_key: str, api_secret: str): | |
| """ | |
| Render testnet tab with client-side JavaScript API calls. | |
| This bypasses HuggingFace's geo-restrictions by making API calls | |
| from the user's browser instead of the server. | |
| """ | |
| # JavaScript/HTML component for client-side API calls | |
| html_code = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <script src="https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.js"></script> | |
| <style> | |
| body {{ | |
| font-family: 'Source Sans Pro', sans-serif; | |
| padding: 20px; | |
| background-color: #0E1117; | |
| color: #FAFAFA; | |
| }} | |
| .portfolio-card {{ | |
| background-color: #262730; | |
| border-radius: 8px; | |
| padding: 20px; | |
| margin: 10px 0; | |
| border: 1px solid #464646; | |
| }} | |
| .metric {{ | |
| text-align: center; | |
| padding: 15px; | |
| }} | |
| .metric-label {{ | |
| font-size: 14px; | |
| color: #A0A0A0; | |
| margin-bottom: 5px; | |
| }} | |
| .metric-value {{ | |
| font-size: 28px; | |
| font-weight: bold; | |
| color: #FAFAFA; | |
| }} | |
| .metric-delta {{ | |
| font-size: 14px; | |
| margin-top: 5px; | |
| }} | |
| .positive {{ color: #00D26A; }} | |
| .negative {{ color: #FF4B4B; }} | |
| .button {{ | |
| padding: 10px 20px; | |
| margin: 5px; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| font-weight: bold; | |
| }} | |
| .buy-btn {{ | |
| background-color: #00D26A; | |
| color: white; | |
| }} | |
| .sell-btn {{ | |
| background-color: #FF4B4B; | |
| color: white; | |
| }} | |
| .info-btn {{ | |
| background-color: #4A4A4A; | |
| color: white; | |
| }} | |
| #status {{ | |
| padding: 10px; | |
| margin: 10px 0; | |
| border-radius: 4px; | |
| display: none; | |
| }} | |
| .status-success {{ | |
| background-color: #00D26A20; | |
| border: 1px solid #00D26A; | |
| color: #00D26A; | |
| }} | |
| .status-error {{ | |
| background-color: #FF4B4B20; | |
| border: 1px solid #FF4B4B; | |
| color: #FF4B4B; | |
| }} | |
| .loading {{ | |
| text-align: center; | |
| padding: 20px; | |
| color: #A0A0A0; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="status"></div> | |
| <div id="loading" class="loading">Loading testnet data...</div> | |
| <div id="portfolio" style="display:none;"> | |
| <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;"> | |
| <div class="portfolio-card metric"> | |
| <div class="metric-label">๐ฐ Portfolio Value</div> | |
| <div class="metric-value" id="portfolioValue">$0.00</div> | |
| <div class="metric-delta" id="portfolioDelta"></div> | |
| </div> | |
| <div class="portfolio-card metric"> | |
| <div class="metric-label">๐ต USDT Balance</div> | |
| <div class="metric-value" id="usdtBalance">$0.00</div> | |
| </div> | |
| <div class="portfolio-card metric"> | |
| <div class="metric-label">๐ Total P&L %</div> | |
| <div class="metric-value" id="totalPnl">+0.00%</div> | |
| <div class="metric-delta" id="pnlDelta"></div> | |
| </div> | |
| </div> | |
| <div class="portfolio-card" style="margin-top: 20px;"> | |
| <h3>๐ Current Positions</h3> | |
| <div id="positions"></div> | |
| </div> | |
| <div class="portfolio-card" style="margin-top: 20px;"> | |
| <h3>๐ฎ Trading Controls</h3> | |
| <select id="symbol" style="padding: 10px; margin: 10px 0; width: 200px;"> | |
| <option value="BTCUSDT">BTC/USDT</option> | |
| <option value="ETHUSDT">ETH/USDT</option> | |
| <option value="SOLUSDT">SOL/USDT</option> | |
| <option value="XRPUSDT">XRP/USDT</option> | |
| </select> | |
| <input type="number" id="amount" placeholder="Amount (USDT)" value="100" style="padding: 10px; margin: 10px; width: 200px;"> | |
| <br> | |
| <button class="button buy-btn" onclick="placeBuyOrder()">๐ข BUY (Market)</button> | |
| <button class="button sell-btn" onclick="placeSellOrder()">๐ด SELL (Market)</button> | |
| <button class="button info-btn" onclick="refreshData()">๐ Refresh</button> | |
| </div> | |
| </div> | |
| <script> | |
| const API_KEY = '{api_key}'; | |
| const API_SECRET = '{api_secret}'; | |
| const BASE_URL = 'https://testnet.binance.vision'; | |
| // Generate HMAC SHA256 signature | |
| function generateSignature(queryString) {{ | |
| return CryptoJS.HmacSHA256(queryString, API_SECRET).toString(); | |
| }} | |
| // Make authenticated API request | |
| async function binanceRequest(endpoint, params = {{}}, method = 'GET') {{ | |
| const timestamp = Date.now(); | |
| params.timestamp = timestamp; | |
| params.recvWindow = 10000; | |
| const queryString = Object.keys(params) | |
| .map(key => `${{key}}=${{params[key]}}`) | |
| .join('&'); | |
| const signature = generateSignature(queryString); | |
| const url = `${{BASE_URL}}${{endpoint}}?${{queryString}}&signature=${{signature}}`; | |
| const response = await fetch(url, {{ | |
| method: method, | |
| headers: {{ | |
| 'X-MBX-APIKEY': API_KEY | |
| }} | |
| }}); | |
| if (!response.ok) {{ | |
| const error = await response.json(); | |
| throw new Error(error.msg || 'API request failed'); | |
| }} | |
| return response.json(); | |
| }} | |
| // Show status message | |
| function showStatus(message, isError = false) {{ | |
| const status = document.getElementById('status'); | |
| status.textContent = message; | |
| status.className = isError ? 'status-error' : 'status-success'; | |
| status.style.display = 'block'; | |
| setTimeout(() => {{ status.style.display = 'none'; }}, 5000); | |
| }} | |
| // Fetch account balance | |
| async function fetchBalance() {{ | |
| try {{ | |
| const account = await binanceRequest('/api/v3/account'); | |
| return account.balances.filter(b => parseFloat(b.free) > 0 || parseFloat(b.locked) > 0); | |
| }} catch (error) {{ | |
| console.error('Error fetching balance:', error); | |
| throw error; | |
| }} | |
| }} | |
| // Fetch ticker price | |
| async function fetchPrice(symbol) {{ | |
| try {{ | |
| const response = await fetch(`${{BASE_URL}}/api/v3/ticker/24hr?symbol=${{symbol}}`); | |
| const ticker = await response.json(); | |
| return parseFloat(ticker.lastPrice); | |
| }} catch (error) {{ | |
| console.error('Error fetching price:', error); | |
| return 0; | |
| }} | |
| }} | |
| // Update portfolio display | |
| async function updatePortfolio() {{ | |
| try {{ | |
| const balances = await fetchBalance(); | |
| let totalValue = 0; | |
| let positions = []; | |
| for (const balance of balances) {{ | |
| const free = parseFloat(balance.free); | |
| const locked = parseFloat(balance.locked); | |
| const total = free + locked; | |
| if (total > 0) {{ | |
| if (balance.asset === 'USDT') {{ | |
| totalValue += total; | |
| document.getElementById('usdtBalance').textContent = `$${{total.toFixed(2)}}`; | |
| }} else {{ | |
| const price = await fetchPrice(balance.asset + 'USDT'); | |
| const value = total * price; | |
| totalValue += value; | |
| positions.push({{ | |
| asset: balance.asset, | |
| amount: total.toFixed(6), | |
| price: price.toFixed(2), | |
| value: value.toFixed(2) | |
| }}); | |
| }} | |
| }} | |
| }} | |
| // Update portfolio metrics | |
| document.getElementById('portfolioValue').textContent = `$${{totalValue.toFixed(2)}}`; | |
| document.getElementById('totalPnl').textContent = 'N/A'; | |
| document.getElementById('totalPnl').className = 'metric-value'; | |
| document.getElementById('portfolioDelta').textContent = ''; | |
| document.getElementById('portfolioDelta').className = 'metric-delta'; | |
| document.getElementById('pnlDelta').textContent = ''; | |
| document.getElementById('pnlDelta').className = 'metric-delta'; | |
| // Update positions table | |
| const positionsDiv = document.getElementById('positions'); | |
| if (positions.length === 0) {{ | |
| positionsDiv.innerHTML = '<p style="color: #A0A0A0;">No open positions. Portfolio is 100% USDT</p>'; | |
| }} else {{ | |
| let html = '<table style="width: 100%; border-collapse: collapse;">'; | |
| html += '<tr><th>Asset</th><th>Amount</th><th>Price</th><th>Value (USDT)</th></tr>'; | |
| positions.forEach(p => {{ | |
| html += `<tr><td>${{p.asset}}</td><td>${{p.amount}}</td><td>$${{p.price}}</td><td>$${{p.value}}</td></tr>`; | |
| }}); | |
| html += '</table>'; | |
| positionsDiv.innerHTML = html; | |
| }} | |
| document.getElementById('loading').style.display = 'none'; | |
| document.getElementById('portfolio').style.display = 'block'; | |
| }} catch (error) {{ | |
| document.getElementById('loading').textContent = 'โ Error: ' + error.message; | |
| showStatus('Error loading portfolio: ' + error.message, true); | |
| }} | |
| }} | |
| // Place buy order | |
| async function placeBuyOrder() {{ | |
| try {{ | |
| const symbol = document.getElementById('symbol').value; | |
| const amountUSDT = parseFloat(document.getElementById('amount').value); | |
| const price = await fetchPrice(symbol); | |
| const quantity = (amountUSDT / price).toFixed(6); | |
| await binanceRequest('/api/v3/order', {{ | |
| symbol: symbol, | |
| side: 'BUY', | |
| type: 'MARKET', | |
| quantity: quantity | |
| }}, 'POST'); | |
| showStatus(`โ BUY order placed: ${{quantity}} ${{symbol.replace('USDT', '')}} @ $${{price.toFixed(2)}}`); | |
| setTimeout(updatePortfolio, 1000); | |
| }} catch (error) {{ | |
| showStatus('โ Order failed: ' + error.message, true); | |
| }} | |
| }} | |
| // Place sell order | |
| async function placeSellOrder() {{ | |
| try {{ | |
| const symbol = document.getElementById('symbol').value; | |
| const balances = await fetchBalance(); | |
| const asset = symbol.replace('USDT', ''); | |
| const balance = balances.find(b => b.asset === asset); | |
| if (!balance || parseFloat(balance.free) === 0) {{ | |
| showStatus(`โ No ${{asset}} to sell`, true); | |
| return; | |
| }} | |
| const quantity = parseFloat(balance.free).toFixed(6); | |
| await binanceRequest('/api/v3/order', {{ | |
| symbol: symbol, | |
| side: 'SELL', | |
| type: 'MARKET', | |
| quantity: quantity | |
| }}, 'POST'); | |
| showStatus(`โ SELL order placed: ${{quantity}} ${{asset}}`); | |
| setTimeout(updatePortfolio, 1000); | |
| }} catch (error) {{ | |
| showStatus('โ Order failed: ' + error.message, true); | |
| }} | |
| }} | |
| // Refresh data | |
| function refreshData() {{ | |
| document.getElementById('loading').style.display = 'block'; | |
| document.getElementById('portfolio').style.display = 'none'; | |
| updatePortfolio(); | |
| }} | |
| // Initialize on load | |
| updatePortfolio(); | |
| // Auto-refresh every 30 seconds | |
| setInterval(updatePortfolio, 30000); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # Render the component | |
| components.html(html_code, height=800, scrolling=True) | |