|
51 | 51 | <button type="button" class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700" data-tab="orders"> |
52 | 52 | Orders |
53 | 53 | </button> |
| 54 | + <button type="button" class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700" data-tab="positions"> |
| 55 | + Positions |
| 56 | + </button> |
54 | 57 | <button type="button" class="tab-btn px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700" data-tab="bots"> |
55 | 58 | Bots |
56 | 59 | </button> |
|
105 | 108 | </div> |
106 | 109 | </div> |
107 | 110 |
|
| 111 | + <!-- Tab Content: Positions --> |
| 112 | + <div id="tab-positions" class="tab-content hidden"> |
| 113 | + <div class="bg-white border border-gray-200 rounded"> |
| 114 | + <div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between"> |
| 115 | + <span class="font-medium">Positions</span> |
| 116 | + <button type="button" id="refresh-positions" class="text-blue-600 hover:text-blue-800 text-sm cursor-pointer"> |
| 117 | + <i class="fas fa-sync-alt mr-1"></i> Refresh |
| 118 | + </button> |
| 119 | + </div> |
| 120 | + |
| 121 | + <div id="positions-content"> |
| 122 | + <div class="p-8 text-center text-gray-400"> |
| 123 | + <i class="fas fa-spinner fa-spin text-4xl mb-3"></i> |
| 124 | + <p>Loading positions...</p> |
| 125 | + </div> |
| 126 | + </div> |
| 127 | + </div> |
| 128 | + </div> |
| 129 | + |
108 | 130 | <!-- Tab Content: Bots --> |
109 | 131 | <div id="tab-bots" class="tab-content hidden"> |
110 | 132 | <div class="bg-white border border-gray-200 rounded"> |
@@ -203,6 +225,11 @@ document.querySelectorAll('.tab-btn').forEach(btn => { |
203 | 225 | if (tabId === 'orders' && !window.ordersLoaded) { |
204 | 226 | loadOrders(); |
205 | 227 | } |
| 228 | +
|
| 229 | + // Load positions when tab is first shown |
| 230 | + if (tabId === 'positions' && !window.positionsLoaded) { |
| 231 | + loadPositions(); |
| 232 | + } |
206 | 233 | }); |
207 | 234 | }); |
208 | 235 |
|
@@ -476,4 +503,146 @@ document.getElementById('refresh-orders').addEventListener('click', function() { |
476 | 503 | window.ordersLoaded = false; |
477 | 504 | loadOrders(); |
478 | 505 | }); |
| 506 | +
|
| 507 | +function loadPositions() { |
| 508 | + const contentEl = document.getElementById('positions-content'); |
| 509 | +
|
| 510 | + if (!hasCredentials) { |
| 511 | + contentEl.innerHTML = ` |
| 512 | + <div class="p-4"> |
| 513 | + <div class="bg-yellow-50 border border-yellow-200 text-yellow-700 px-4 py-3 rounded"> |
| 514 | + <i class="fas fa-info-circle mr-2"></i> API credentials required to fetch positions. |
| 515 | + <a href="/profiles/${profileId}/edit" class="underline">Configure now</a> |
| 516 | + </div> |
| 517 | + </div> |
| 518 | + `; |
| 519 | + return; |
| 520 | + } |
| 521 | +
|
| 522 | + contentEl.innerHTML = ` |
| 523 | + <div class="p-8 text-center text-gray-400"> |
| 524 | + <i class="fas fa-spinner fa-spin text-4xl mb-3"></i> |
| 525 | + <p>Loading positions...</p> |
| 526 | + </div> |
| 527 | + `; |
| 528 | +
|
| 529 | + fetch('/api/profiles/' + profileId + '/positions') |
| 530 | + .then(res => res.json()) |
| 531 | + .then(data => { |
| 532 | + if (data.error) { |
| 533 | + contentEl.innerHTML = ` |
| 534 | + <div class="p-4"> |
| 535 | + <div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded"> |
| 536 | + <i class="fas fa-exclamation-circle mr-2"></i>${data.error} |
| 537 | + </div> |
| 538 | + </div> |
| 539 | + `; |
| 540 | + return; |
| 541 | + } |
| 542 | +
|
| 543 | + const positions = data.positions || []; |
| 544 | +
|
| 545 | + if (positions.length === 0) { |
| 546 | + contentEl.innerHTML = ` |
| 547 | + <div class="p-8 text-center text-gray-400"> |
| 548 | + <i class="fas fa-chart-line text-4xl mb-3"></i> |
| 549 | + <p>No open positions.</p> |
| 550 | + </div> |
| 551 | + `; |
| 552 | + return; |
| 553 | + } |
| 554 | +
|
| 555 | + let html = ` |
| 556 | + <table class="w-full text-sm"> |
| 557 | + <thead> |
| 558 | + <tr class="bg-gray-50 border-b border-gray-200"> |
| 559 | + <th class="text-left py-2 px-4 font-medium text-gray-600">Symbol</th> |
| 560 | + <th class="text-left py-2 px-4 font-medium text-gray-600">Side</th> |
| 561 | + <th class="text-right py-2 px-4 font-medium text-gray-600">Contracts</th> |
| 562 | + <th class="text-right py-2 px-4 font-medium text-gray-600">Entry</th> |
| 563 | + <th class="text-right py-2 px-4 font-medium text-gray-600">Mark</th> |
| 564 | + <th class="text-right py-2 px-4 font-medium text-gray-600">Notional</th> |
| 565 | + <th class="text-right py-2 px-4 font-medium text-gray-600">Unreal. PnL</th> |
| 566 | + <th class="text-right py-2 px-4 font-medium text-gray-600">Liq. Price</th> |
| 567 | + <th class="text-right py-2 px-4 font-medium text-gray-600">Lev.</th> |
| 568 | + <th class="text-left py-2 px-4 font-medium text-gray-600">Margin</th> |
| 569 | + <th class="text-left py-2 px-4 font-medium text-gray-600">Action</th> |
| 570 | + </tr> |
| 571 | + </thead> |
| 572 | + <tbody class="divide-y divide-gray-200"> |
| 573 | + `; |
| 574 | +
|
| 575 | + positions.forEach(function(pos) { |
| 576 | + const sideBadge = pos.side === 'long' |
| 577 | + ? '<span class="px-2 py-1 rounded text-xs bg-green-100 text-green-800"><i class="fas fa-chevron-up mr-1"></i>long</span>' |
| 578 | + : '<span class="px-2 py-1 rounded text-xs bg-red-100 text-red-800"><i class="fas fa-chevron-down mr-1"></i>short</span>'; |
| 579 | +
|
| 580 | + const pnlClass = pos.unrealizedPnl >= 0 ? 'text-green-600' : 'text-red-600'; |
| 581 | + const pnlSign = pos.unrealizedPnl >= 0 ? '+' : ''; |
| 582 | +
|
| 583 | + html += ` |
| 584 | + <tr class="hover:bg-gray-50"> |
| 585 | + <td class="py-2 px-4 font-mono text-xs">${pos.symbol}</td> |
| 586 | + <td class="py-2 px-4">${sideBadge}</td> |
| 587 | + <td class="py-2 px-4 text-right font-mono">${pos.contracts}${pos.contractSize && pos.contractSize !== 1 ? '<span class="text-gray-400 text-xs">×' + pos.contractSize + '</span>' : ''}</td> |
| 588 | + <td class="py-2 px-4 text-right font-mono">${pos.entryPrice != null ? formatPrice(pos.entryPrice) : '-'}</td> |
| 589 | + <td class="py-2 px-4 text-right font-mono">${pos.markPrice != null ? formatPrice(pos.markPrice) : '-'}</td> |
| 590 | + <td class="py-2 px-4 text-right font-mono">${pos.notional != null ? formatPrice(Math.abs(pos.notional)) : '-'}</td> |
| 591 | + <td class="py-2 px-4 text-right font-mono ${pnlClass}"> |
| 592 | + ${pos.unrealizedPnl != null ? pnlSign + pos.unrealizedPnl.toFixed(4) + (pos.percentage != null ? '<span class="text-xs opacity-75">(' + pos.percentage.toFixed(2) + '%)</span>' : '') : '-'} |
| 593 | + </td> |
| 594 | + <td class="py-2 px-4 text-right font-mono text-orange-600">${pos.liquidationPrice != null ? formatPrice(pos.liquidationPrice) : '-'}</td> |
| 595 | + <td class="py-2 px-4 text-right text-gray-600">${pos.leverage != null ? pos.leverage + '×' : '-'}</td> |
| 596 | + <td class="py-2 px-4 text-gray-500">${pos.marginMode ? '<span class="px-2 py-1 rounded text-xs bg-gray-100 text-gray-600">' + pos.marginMode + '</span>' : '-'}</td> |
| 597 | + <td class="py-2 px-4 whitespace-nowrap"> |
| 598 | + <button onclick="closePosition('${encodeURIComponent(pos.symbol)}', 'limit')" class="text-xs px-2 py-1 rounded bg-yellow-100 text-yellow-800 hover:bg-yellow-200 mr-1" title="Limit Close" onclick="return confirm('Limit close ${pos.contracts} ${pos.symbol}?')">Limit</button> |
| 599 | + <button onclick="closePosition('${encodeURIComponent(pos.symbol)}', 'market')" class="text-xs px-2 py-1 rounded bg-red-100 text-red-800 hover:bg-red-200" title="Market Close">Market</button> |
| 600 | + </td> |
| 601 | + </tr> |
| 602 | + `; |
| 603 | + }); |
| 604 | +
|
| 605 | + html += '</tbody></table>'; |
| 606 | + contentEl.innerHTML = html; |
| 607 | + window.positionsLoaded = true; |
| 608 | + }) |
| 609 | + .catch(err => { |
| 610 | + contentEl.innerHTML = ` |
| 611 | + <div class="p-4"> |
| 612 | + <div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded"> |
| 613 | + <i class="fas fa-exclamation-circle mr-2"></i>Failed to load positions: ${err.message} |
| 614 | + </div> |
| 615 | + </div> |
| 616 | + `; |
| 617 | + }); |
| 618 | +} |
| 619 | +
|
| 620 | +function closePosition(symbol, type) { |
| 621 | + const typeLabel = type === 'market' ? 'Market' : 'Limit'; |
| 622 | + if (!confirm(typeLabel + ' close position for ' + decodeURIComponent(symbol) + '?')) return; |
| 623 | +
|
| 624 | + fetch('/api/profiles/' + profileId + '/positions/close', { |
| 625 | + method: 'POST', |
| 626 | + headers: { 'Content-Type': 'application/json' }, |
| 627 | + body: JSON.stringify({ symbol: symbol, type: type }) |
| 628 | + }) |
| 629 | + .then(res => res.json()) |
| 630 | + .then(data => { |
| 631 | + if (data.error) { |
| 632 | + alert('Failed to close position: ' + data.error); |
| 633 | + } else { |
| 634 | + window.positionsLoaded = false; |
| 635 | + loadPositions(); |
| 636 | + } |
| 637 | + }) |
| 638 | + .catch(err => { |
| 639 | + alert('Failed to close position: ' + err.message); |
| 640 | + }); |
| 641 | +} |
| 642 | +
|
| 643 | +// Refresh positions button |
| 644 | +document.getElementById('refresh-positions').addEventListener('click', function() { |
| 645 | + window.positionsLoaded = false; |
| 646 | + loadPositions(); |
| 647 | +}); |
479 | 648 | </script> |
0 commit comments