- Sidebar portfolio con metriche dettagliate (Totale, Investito, Disponibile, P&L, ROI) e aggiornamento real-time - Sistema multi-strategia: 8 strategie assegnabili per asset, voting decisionale, pagina Trading Control - Nuova pagina Posizioni: gestione, chiusura manuale, P&L non realizzato, notifiche - Sistema indicatori tecnici: 7+ indicatori configurabili, segnali real-time, raccomandazioni, storico segnali - Refactoring TradingBotService per capitale, P&L, ROI, eventi - Nuovi modelli e servizi per strategie/indicatori, persistenza configurazioni - UI/UX: navigazione aggiornata, widget, modali, responsive - Aggiornamento README e CHANGELOG con tutte le novità
717 lines
21 KiB
Plaintext
717 lines
21 KiB
Plaintext
@page "/positions"
|
||
@using TradingBot.Services
|
||
@using TradingBot.Models
|
||
@inject TradingBotService BotService
|
||
@inject LoggingService LoggingService
|
||
@implements IDisposable
|
||
@rendermode InteractiveServer
|
||
|
||
<PageTitle>Posizioni Aperte - TradingBot</PageTitle>
|
||
|
||
<div class="positions-page">
|
||
<div class="page-header">
|
||
<div>
|
||
<h1>Posizioni Aperte</h1>
|
||
<p class="subtitle">Gestisci le tue posizioni attive - Solo chiusura manuale disponibile</p>
|
||
</div>
|
||
<div class="header-stats">
|
||
<div class="stat-card">
|
||
<span class="stat-label">Posizioni Attive</span>
|
||
<span class="stat-value">@activePositions.Count</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-label">Valore Totale</span>
|
||
<span class="stat-value">$@totalValue.ToString("N2")</span>
|
||
</div>
|
||
<div class="stat-card">
|
||
<span class="stat-label">P&L Non Realizzato</span>
|
||
<span class="stat-value @(totalUnrealizedPL >= 0 ? "profit" : "loss")">
|
||
@(totalUnrealizedPL >= 0 ? "+" : "")$@totalUnrealizedPL.ToString("N2")
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@if (activePositions.Count == 0)
|
||
{
|
||
<div class="empty-state">
|
||
<div class="empty-icon">
|
||
<span class="bi bi-inbox"></span>
|
||
</div>
|
||
<h3>Nessuna Posizione Aperta</h3>
|
||
<p>Non hai posizioni attive al momento. Le posizioni appaiono qui automaticamente quando il bot esegue un acquisto.</p>
|
||
<div class="empty-actions">
|
||
<a href="/trading-control" class="btn-primary">
|
||
<span class="bi bi-sliders"></span>
|
||
Configura Trading
|
||
</a>
|
||
<a href="/dashboard" class="btn-secondary">
|
||
<span class="bi bi-speedometer2"></span>
|
||
Dashboard
|
||
</a>
|
||
</div>
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<div class="positions-grid">
|
||
@foreach (var position in activePositions.OrderBy(p => p.Symbol))
|
||
{
|
||
var currentPrice = GetCurrentPrice(position.Symbol);
|
||
var unrealizedPL = CalculateUnrealizedPL(position, currentPrice);
|
||
var plPercentage = CalculatePLPercentage(position, currentPrice);
|
||
var currentValue = position.Amount * currentPrice;
|
||
var holdingTime = DateTime.UtcNow - position.Timestamp;
|
||
|
||
<div class="position-card">
|
||
<div class="position-header">
|
||
<div class="position-asset">
|
||
<h3>@position.Symbol</h3>
|
||
<span class="position-date">
|
||
Aperta @position.Timestamp.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
||
</span>
|
||
</div>
|
||
<div class="position-pl @(unrealizedPL >= 0 ? "profit" : "loss")">
|
||
<span class="pl-value">
|
||
@(unrealizedPL >= 0 ? "+" : "")$@unrealizedPL.ToString("N2")
|
||
</span>
|
||
<span class="pl-percentage">
|
||
(@(plPercentage >= 0 ? "+" : "")@plPercentage.ToString("F2")%)
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="position-details">
|
||
<div class="detail-row">
|
||
<span class="detail-label">Quantità</span>
|
||
<span class="detail-value">@position.Amount.ToString("F8") @position.Symbol</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Prezzo Entrata</span>
|
||
<span class="detail-value">$@position.Price.ToString("N2")</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Prezzo Corrente</span>
|
||
<span class="detail-value">$@currentPrice.ToString("N2")</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Valore Iniziale</span>
|
||
<span class="detail-value">$@(position.Amount * position.Price).ToString("N2")</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Valore Corrente</span>
|
||
<span class="detail-value">$@currentValue.ToString("N2")</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Tempo Holding</span>
|
||
<span class="detail-value">@FormatHoldingTime(holdingTime)</span>
|
||
</div>
|
||
@if (!string.IsNullOrEmpty(position.Strategy))
|
||
{
|
||
<div class="detail-row">
|
||
<span class="detail-label">Strategia</span>
|
||
<span class="detail-value strategy-badge">@position.Strategy</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="position-actions">
|
||
<button class="btn-danger" @onclick="() => ShowCloseConfirmation(position)"
|
||
disabled="@(!BotService.Status.IsRunning)">
|
||
<span class="bi bi-x-circle"></span>
|
||
Chiudi Posizione
|
||
</button>
|
||
@if (!BotService.Status.IsRunning)
|
||
{
|
||
<span class="action-note">
|
||
<span class="bi bi-info-circle"></span>
|
||
Avvia il bot per chiudere posizioni
|
||
</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
<!-- Close Confirmation Modal -->
|
||
@if (showCloseModal && positionToClose != null)
|
||
{
|
||
var currentPrice = GetCurrentPrice(positionToClose.Symbol);
|
||
var unrealizedPL = CalculateUnrealizedPL(positionToClose, currentPrice);
|
||
var plPercentage = CalculatePLPercentage(positionToClose, currentPrice);
|
||
|
||
<div class="modal-overlay" @onclick="HideCloseConfirmation">
|
||
<div class="modal-dialog" @onclick:stopPropagation="true">
|
||
<div class="modal-header">
|
||
<h3>Conferma Chiusura Posizione</h3>
|
||
<button class="btn-close" @onclick="HideCloseConfirmation">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="confirmation-details">
|
||
<div class="confirm-asset">
|
||
<h4>@positionToClose.Symbol</h4>
|
||
<span class="confirm-amount">@positionToClose.Amount.ToString("F8") @positionToClose.Symbol</span>
|
||
</div>
|
||
|
||
<div class="confirm-prices">
|
||
<div class="price-item">
|
||
<span class="price-label">Prezzo Entrata</span>
|
||
<span class="price-value">$@positionToClose.Price.ToString("N2")</span>
|
||
</div>
|
||
<span class="bi bi-arrow-right"></span>
|
||
<div class="price-item">
|
||
<span class="price-label">Prezzo Chiusura</span>
|
||
<span class="price-value">$@currentPrice.ToString("N2")</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="confirm-pl @(unrealizedPL >= 0 ? "profit" : "loss")">
|
||
<div class="pl-label">Profitto/Perdita Stimato</div>
|
||
<div class="pl-amount">
|
||
@(unrealizedPL >= 0 ? "+" : "")$@unrealizedPL.ToString("N2")
|
||
</div>
|
||
<div class="pl-percent">
|
||
(@(plPercentage >= 0 ? "+" : "")@plPercentage.ToString("F2")%)
|
||
</div>
|
||
</div>
|
||
|
||
<div class="confirm-warning">
|
||
<span class="bi bi-exclamation-triangle"></span>
|
||
<p>
|
||
<strong>Attenzione:</strong> Questa azione chiuderà immediatamente la posizione al prezzo di mercato corrente.
|
||
L'operazione non può essere annullata.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn-secondary" @onclick="HideCloseConfirmation">Annulla</button>
|
||
<button class="btn-danger" @onclick="ConfirmClosePosition">
|
||
<span class="bi bi-x-circle"></span>
|
||
Conferma Chiusura
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Success Notification -->
|
||
@if (showSuccessNotification)
|
||
{
|
||
<div class="notification success">
|
||
<span class="bi bi-check-circle-fill"></span>
|
||
Posizione chiusa con successo!
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<style>
|
||
.positions-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2rem;
|
||
}
|
||
|
||
.header-stats {
|
||
display: flex;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.stat-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
padding: 1rem 1.5rem;
|
||
background: #1a1f3a;
|
||
border-radius: 0.75rem;
|
||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.875rem;
|
||
color: #94a3b8;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 1.75rem;
|
||
font-weight: 700;
|
||
color: #e2e8f0;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.stat-value.profit {
|
||
color: #10b981;
|
||
}
|
||
|
||
.stat-value.loss {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.empty-state {
|
||
background: #1a1f3a;
|
||
border-radius: 0.75rem;
|
||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||
padding: 4rem 2rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 4rem;
|
||
color: #64748b;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.empty-state h3 {
|
||
color: #e2e8f0;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.empty-state p {
|
||
color: #94a3b8;
|
||
margin-bottom: 2rem;
|
||
max-width: 600px;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
}
|
||
|
||
.empty-actions {
|
||
display: flex;
|
||
gap: 1rem;
|
||
justify-content: center;
|
||
}
|
||
|
||
.positions-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.position-card {
|
||
background: #1a1f3a;
|
||
border-radius: 0.75rem;
|
||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||
padding: 1.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.5rem;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.position-card:hover {
|
||
border-color: rgba(99, 102, 241, 0.4);
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.position-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
padding-bottom: 1rem;
|
||
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
|
||
}
|
||
|
||
.position-asset h3 {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
color: #e2e8f0;
|
||
margin: 0 0 0.25rem 0;
|
||
}
|
||
|
||
.position-date {
|
||
font-size: 0.75rem;
|
||
color: #64748b;
|
||
}
|
||
|
||
.position-pl {
|
||
text-align: right;
|
||
}
|
||
|
||
.pl-value {
|
||
display: block;
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.position-pl.profit .pl-value {
|
||
color: #10b981;
|
||
}
|
||
|
||
.position-pl.loss .pl-value {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.pl-percentage {
|
||
font-size: 0.875rem;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.position-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.detail-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.detail-label {
|
||
font-size: 0.875rem;
|
||
color: #94a3b8;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.detail-value {
|
||
font-size: 0.875rem;
|
||
color: #e2e8f0;
|
||
font-family: 'Courier New', monospace;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.strategy-badge {
|
||
padding: 0.25rem 0.75rem;
|
||
background: rgba(99, 102, 241, 0.2);
|
||
border-radius: 0.25rem;
|
||
color: #6366f1;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.position-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid rgba(99, 102, 241, 0.1);
|
||
}
|
||
|
||
.action-note {
|
||
font-size: 0.75rem;
|
||
color: #f59e0b;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
/* Modal Styles */
|
||
.confirmation-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.confirm-asset {
|
||
text-align: center;
|
||
padding: 1rem;
|
||
background: rgba(99, 102, 241, 0.1);
|
||
border-radius: 0.5rem;
|
||
}
|
||
|
||
.confirm-asset h4 {
|
||
font-size: 1.5rem;
|
||
color: #e2e8f0;
|
||
margin: 0 0 0.5rem 0;
|
||
}
|
||
|
||
.confirm-amount {
|
||
color: #94a3b8;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.confirm-prices {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
padding: 1rem;
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 0.5rem;
|
||
}
|
||
|
||
.price-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.price-label {
|
||
font-size: 0.75rem;
|
||
color: #64748b;
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.price-value {
|
||
font-size: 1.25rem;
|
||
color: #e2e8f0;
|
||
font-weight: 700;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
|
||
.confirm-prices .bi {
|
||
font-size: 1.5rem;
|
||
color: #6366f1;
|
||
}
|
||
|
||
.confirm-pl {
|
||
text-align: center;
|
||
padding: 1.5rem;
|
||
border-radius: 0.5rem;
|
||
}
|
||
|
||
.confirm-pl.profit {
|
||
background: rgba(16, 185, 129, 0.1);
|
||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||
}
|
||
|
||
.confirm-pl.loss {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||
}
|
||
|
||
.pl-label {
|
||
font-size: 0.875rem;
|
||
color: #94a3b8;
|
||
margin-bottom: 0.5rem;
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.pl-amount {
|
||
font-size: 2rem;
|
||
font-weight: 700;
|
||
font-family: 'Courier New', monospace;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.confirm-pl.profit .pl-amount {
|
||
color: #10b981;
|
||
}
|
||
|
||
.confirm-pl.loss .pl-amount {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.pl-percent {
|
||
font-size: 1rem;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.confirm-warning {
|
||
display: flex;
|
||
gap: 1rem;
|
||
padding: 1rem;
|
||
background: rgba(245, 158, 11, 0.1);
|
||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||
border-radius: 0.5rem;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.confirm-warning .bi {
|
||
font-size: 1.25rem;
|
||
color: #f59e0b;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.confirm-warning p {
|
||
margin: 0;
|
||
font-size: 0.875rem;
|
||
color: #cbd5e1;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.confirm-warning strong {
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.notification {
|
||
position: fixed;
|
||
bottom: 2rem;
|
||
right: 2rem;
|
||
padding: 1rem 1.5rem;
|
||
border-radius: 0.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
font-weight: 600;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||
z-index: 9999;
|
||
animation: slideInRight 0.3s ease;
|
||
}
|
||
|
||
.notification.success {
|
||
background: rgba(16, 185, 129, 0.2);
|
||
border: 1px solid #10b981;
|
||
color: #10b981;
|
||
}
|
||
|
||
@@keyframes slideInRight {
|
||
from {
|
||
transform: translateX(400px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@@media (max-width: 768px) {
|
||
.positions-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.header-stats {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.confirm-prices {
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.confirm-prices .bi {
|
||
transform: rotate(90deg);
|
||
}
|
||
}
|
||
</style>
|
||
|
||
@code {
|
||
private List<Trade> activePositions = new();
|
||
private decimal totalValue = 0;
|
||
private decimal totalUnrealizedPL = 0;
|
||
private bool showCloseModal = false;
|
||
private Trade? positionToClose = null;
|
||
private bool showSuccessNotification = false;
|
||
|
||
protected override void OnInitialized()
|
||
{
|
||
LoadPositions();
|
||
BotService.OnTradeExecuted += HandleTradeExecuted;
|
||
BotService.OnPriceUpdated += HandlePriceUpdated;
|
||
BotService.OnStatusChanged += HandleStatusChanged;
|
||
}
|
||
|
||
private void LoadPositions()
|
||
{
|
||
activePositions = BotService.ActivePositions.Values.ToList();
|
||
CalculateTotals();
|
||
}
|
||
|
||
private void CalculateTotals()
|
||
{
|
||
totalValue = 0;
|
||
totalUnrealizedPL = 0;
|
||
|
||
foreach (var position in activePositions)
|
||
{
|
||
var currentPrice = GetCurrentPrice(position.Symbol);
|
||
var positionValue = position.Amount * currentPrice;
|
||
var pl = CalculateUnrealizedPL(position, currentPrice);
|
||
|
||
totalValue += positionValue;
|
||
totalUnrealizedPL += pl;
|
||
}
|
||
}
|
||
|
||
private decimal GetCurrentPrice(string symbol)
|
||
{
|
||
var latestPrice = BotService.GetLatestPrice(symbol);
|
||
return latestPrice?.Price ?? 0;
|
||
}
|
||
|
||
private decimal CalculateUnrealizedPL(Trade position, decimal currentPrice)
|
||
{
|
||
return (currentPrice - position.Price) * position.Amount;
|
||
}
|
||
|
||
private decimal CalculatePLPercentage(Trade position, decimal currentPrice)
|
||
{
|
||
if (position.Price == 0) return 0;
|
||
return ((currentPrice - position.Price) / position.Price) * 100;
|
||
}
|
||
|
||
private string FormatHoldingTime(TimeSpan time)
|
||
{
|
||
if (time.TotalDays >= 1)
|
||
return $"{(int)time.TotalDays}g {time.Hours}h";
|
||
else if (time.TotalHours >= 1)
|
||
return $"{(int)time.TotalHours}h {time.Minutes}m";
|
||
else
|
||
return $"{(int)time.TotalMinutes}m {time.Seconds}s";
|
||
}
|
||
|
||
private void ShowCloseConfirmation(Trade position)
|
||
{
|
||
positionToClose = position;
|
||
showCloseModal = true;
|
||
}
|
||
|
||
private void HideCloseConfirmation()
|
||
{
|
||
showCloseModal = false;
|
||
positionToClose = null;
|
||
}
|
||
|
||
private async Task ConfirmClosePosition()
|
||
{
|
||
if (positionToClose == null) return;
|
||
|
||
try
|
||
{
|
||
// Close position using TradingBotService public method
|
||
await BotService.ClosePositionManuallyAsync(positionToClose.Symbol);
|
||
|
||
LoggingService.LogInfo(
|
||
"Positions",
|
||
$"Posizione chiusa manualmente: {positionToClose.Symbol}",
|
||
$"Quantità: {positionToClose.Amount:F8}");
|
||
|
||
showSuccessNotification = true;
|
||
HideCloseConfirmation();
|
||
LoadPositions();
|
||
|
||
// Hide notification after 3 seconds
|
||
await Task.Delay(3000);
|
||
showSuccessNotification = false;
|
||
StateHasChanged();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LoggingService.LogError("Positions", $"Errore chiusura posizione: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void HandleTradeExecuted(Trade trade)
|
||
{
|
||
LoadPositions();
|
||
InvokeAsync(StateHasChanged);
|
||
}
|
||
|
||
private void HandlePriceUpdated(string symbol, MarketPrice price)
|
||
{
|
||
if (activePositions.Any(p => p.Symbol == symbol))
|
||
{
|
||
CalculateTotals();
|
||
InvokeAsync(StateHasChanged);
|
||
}
|
||
}
|
||
|
||
private void HandleStatusChanged()
|
||
{
|
||
InvokeAsync(StateHasChanged);
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
BotService.OnTradeExecuted -= HandleTradeExecuted;
|
||
BotService.OnPriceUpdated -= HandlePriceUpdated;
|
||
BotService.OnStatusChanged -= HandleStatusChanged;
|
||
}
|
||
}
|