Skip to content

Commit e636e2a

Browse files
committed
show positions on profiles
1 parent 1d0e594 commit e636e2a

File tree

2 files changed

+211
-0
lines changed

2 files changed

+211
-0
lines changed

src/profile/profile_controller.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export class ProfileController extends BaseController {
3737
// API Routes
3838
router.get('/api/profiles/:id/balances', this.getBalances.bind(this));
3939
router.get('/api/profiles/:id/orders', this.getOrders.bind(this));
40+
router.get('/api/profiles/:id/positions', this.getPositions.bind(this));
41+
router.post('/api/profiles/:id/positions/close', this.closePosition.bind(this));
4042
router.get('/api/profiles/:id/orders/:pair/cancel/:orderId', this.cancelOrder.bind(this));
4143
router.get('/api/exchanges', this.getExchanges.bind(this));
4244
router.get('/api/profiles/:id/pairs', this.getPairs.bind(this));
@@ -185,6 +187,46 @@ export class ProfileController extends BaseController {
185187
}
186188
}
187189

190+
private async getPositions(req: express.Request, res: express.Response): Promise<void> {
191+
const { id } = req.params;
192+
const profile = this.profileService.getProfile(id);
193+
194+
if (!profile) {
195+
res.status(404).json({ error: 'Profile not found' });
196+
return;
197+
}
198+
199+
if (!profile.apiKey || !profile.secret) {
200+
res.status(400).json({ error: 'API credentials not configured' });
201+
return;
202+
}
203+
204+
try {
205+
const positions = await this.profileService.fetchOpenPositions(id);
206+
res.json({ success: true, positions });
207+
} catch (error: any) {
208+
res.status(500).json({ error: error.message || 'Failed to fetch positions' });
209+
}
210+
}
211+
212+
private async closePosition(req: express.Request, res: express.Response): Promise<void> {
213+
const { id } = req.params;
214+
const { symbol, type } = req.body;
215+
const profile = this.profileService.getProfile(id);
216+
217+
if (!profile) {
218+
res.status(404).json({ error: 'Profile not found' });
219+
return;
220+
}
221+
222+
try {
223+
await this.profileService.closePosition(id, decodeURIComponent(symbol), type as 'limit' | 'market');
224+
res.json({ success: true });
225+
} catch (error: any) {
226+
res.status(500).json({ error: error.message || 'Failed to close position' });
227+
}
228+
}
229+
188230
private async getExchanges(req: express.Request, res: express.Response): Promise<void> {
189231
const exchanges = this.profileService.getSupportedExchanges();
190232
res.json({ exchanges });

views/profile/view.ejs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
<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">
5252
Orders
5353
</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>
5457
<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">
5558
Bots
5659
</button>
@@ -105,6 +108,25 @@
105108
</div>
106109
</div>
107110

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+
108130
<!-- Tab Content: Bots -->
109131
<div id="tab-bots" class="tab-content hidden">
110132
<div class="bg-white border border-gray-200 rounded">
@@ -203,6 +225,11 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
203225
if (tabId === 'orders' && !window.ordersLoaded) {
204226
loadOrders();
205227
}
228+
229+
// Load positions when tab is first shown
230+
if (tabId === 'positions' && !window.positionsLoaded) {
231+
loadPositions();
232+
}
206233
});
207234
});
208235
@@ -476,4 +503,146 @@ document.getElementById('refresh-orders').addEventListener('click', function() {
476503
window.ordersLoaded = false;
477504
loadOrders();
478505
});
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+
});
479648
</script>

0 commit comments

Comments
 (0)