Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2026-08-12 - Пользовательские пороги уведомлений и синхронизация между вкладками
**Инсайт:** При реализации фильтрации уведомлений на стороне клиента (например, минимального порога размера сделки в USDT) необходимо: 1) Сохранять пользовательские фильтры в `localStorage` с безопасным фолбэком на дефолтные значения; 2) Рассчитывать полную стоимость сделки (`Amount * Price`) до проверки порога; 3) Генерировать кастомные события Window (`notificationSettingsChanged`) при изменении настроек, чтобы обеспечивать реактивную синхронизацию между открытыми представлениями (Dashboard, Settings) и вкладками.
**Действие:** Фильтровать фоновый опрос сделок по `tradeValue >= minTradeSize`, сохранять параметры в `localStorage` и оповещать связанные UI-элементы через `CustomEvent`.

## 2026-08-11 - Динамическая защита прибыли при высокой волатильности спреда (SellProcessor)
**Инсайт:** Во время резких разворотов рынка и высокой спред-волатильности удерживание стандартной целевой маржи продажи (`SellMargin`) или широкого трейлинг-стопа может привести к быстрой потере накопленной нереализованной прибыли. Динамическое снижение эффективного целевого порога продажи (Sell Margin) и подтягивание (ужесточение) трейлинг-расстояния при повышенном спреде позволяет досрочно зафиксировать прибыль до наступления сильного пролива.
**Действие:** В `SellProcessor` при превышении базового спреда вычислять дисконт спреда (`spreadExcess`), снижая целевую маржу продажи (до 50%) и ужесточая `effectiveTrailing` при активном трейлинге.
Expand Down
80 changes: 68 additions & 12 deletions IntelliTrader.Web/Views/Home/Dashboard.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
</div>
</div>

<!-- Дополнительные настройки звука -->
<!-- Дополнительные настройки звука и фильтрации -->
<div class="row align-items-center mt-2 pt-2" style="border-top: 1px solid var(--card-border); font-size: 13px;">
<div class="col-md-5 d-flex align-items-center mb-2 mb-md-0">
<div class="col-md-4 d-flex align-items-center mb-2 mb-md-0">
<i class="fas fa-sliders-h mr-2" style="color: #7aa2f7;"></i>
<span class="mr-2" style="white-space: nowrap;">Громкость:</span>
<input type="range" id="soundVolumeSlider" min="0" max="1" step="0.05" value="0.5" style="flex-grow: 1; height: 4px; background: var(--bg-color); outline: none; transition: background 450ms ease-in-out; -webkit-appearance: none; cursor: pointer;" />
<span id="soundVolumeValue" class="ml-2" style="font-weight: bold; color: #7aa2f7; min-width: 40px; text-align: right;">50%</span>
</div>
<div class="col-md-5 d-flex align-items-center mb-2 mb-md-0">
<div class="col-md-4 d-flex align-items-center mb-2 mb-md-0">
<i class="fas fa-music mr-2" style="color: #bb9af3;"></i>
<span class="mr-2" style="white-space: nowrap;">Профиль звука:</span>
<select id="soundProfileSelect" class="form-control form-control-sm" style="background-color: var(--input-bg); color: var(--text-color); border: 1px solid var(--input-border); border-radius: 4px; height: 28px; padding: 2px 8px; font-size: 12px; width: auto;">
Expand All @@ -44,8 +44,10 @@
<option value="arcade">Ретро аркада (Arcade)</option>
</select>
</div>
<div class="col-md-2 text-md-right text-muted" style="font-size: 11px;">
Настройки звука
<div class="col-md-4 d-flex align-items-center mb-2 mb-md-0">
<i class="fas fa-filter mr-2" style="color: #e0af68;"></i>
<span class="mr-2" style="white-space: nowrap;">Мин. размер (USDT):</span>
<input type="number" id="minTradeSizeInput" min="0" step="1" value="0" class="form-control form-control-sm" style="background-color: var(--input-bg); color: var(--text-color); border: 1px solid var(--input-border); border-radius: 4px; height: 28px; padding: 2px 8px; font-size: 12px; width: 90px;" />
</div>
</div>
</div>
Expand All @@ -59,6 +61,7 @@
const soundVolumeSlider = document.getElementById("soundVolumeSlider");
const soundVolumeValue = document.getElementById("soundVolumeValue");
const soundProfileSelect = document.getElementById("soundProfileSelect");
const minTradeSizeInput = document.getElementById("minTradeSizeInput");
const notificationContainer = document.getElementById("notificationContainer");

let isSoundEnabled = localStorage.getItem("realtime_sound_enabled") !== "false";
Expand All @@ -74,10 +77,21 @@
// Load sound profile with fallback to 'chime'
let soundProfile = localStorage.getItem("realtime_sound_profile") || "chime";

// Load minimum trade size with fallback to 0
let minTradeSize = localStorage.getItem("realtime_min_trade_size");
if (minTradeSize === null) {
minTradeSize = 0;
} else {
minTradeSize = parseFloat(minTradeSize) || 0;
}

// Sync UI controls with values
soundVolumeSlider.value = soundVolume;
soundVolumeValue.textContent = Math.round(soundVolume * 100) + "%";
soundProfileSelect.value = soundProfile;
if (minTradeSizeInput) {
minTradeSizeInput.value = minTradeSize;
}

updateSoundButtonUI();

Expand Down Expand Up @@ -214,6 +228,41 @@
playNotificationSound();
});

if (minTradeSizeInput) {
minTradeSizeInput.addEventListener("input", function () {
minTradeSize = parseFloat(minTradeSizeInput.value) || 0;
localStorage.setItem("realtime_min_trade_size", minTradeSize);
const event = new CustomEvent("notificationSettingsChanged", {
detail: { minTradeSize, soundVolume, soundProfile, isSoundEnabled }
});
document.dispatchEvent(event);
});
}

document.addEventListener("notificationSettingsChanged", function (e) {
if (e.detail) {
if (e.detail.minTradeSize !== undefined) {
minTradeSize = parseFloat(e.detail.minTradeSize) || 0;
if (minTradeSizeInput && minTradeSizeInput.value !== String(minTradeSize)) {
minTradeSizeInput.value = minTradeSize;
}
}
if (e.detail.soundVolume !== undefined) {
soundVolume = parseFloat(e.detail.soundVolume);
if (soundVolumeSlider) soundVolumeSlider.value = soundVolume;
if (soundVolumeValue) soundVolumeValue.textContent = Math.round(soundVolume * 100) + "%";
}
if (e.detail.soundProfile !== undefined) {
soundProfile = e.detail.soundProfile;
if (soundProfileSelect) soundProfileSelect.value = soundProfile;
}
if (e.detail.isSoundEnabled !== undefined) {
isSoundEnabled = e.detail.isSoundEnabled;
updateSoundButtonUI();
}
}
});

testSoundBtn.addEventListener("click", function () {
initAudio();
const prev = isSoundEnabled;
Expand Down Expand Up @@ -324,25 +373,32 @@
.then(response => response.json())
.then(data => {
if (data.success && data.trades) {
let hasNewTrade = false;
let hasNewTradeToAlert = false;

data.trades.forEach(trade => {
const tradeId = `${trade.Pair}_${trade.SellDate}`;
if (!knownTrades.has(tradeId)) {
knownTrades.add(tradeId);

if (!isFirstLoad) {
hasNewTrade = true;
showToastNotification(trade);
const amount = parseFloat(trade.Amount) || 0;
const price = parseFloat(trade.AveragePrice) || parseFloat(trade.SellPrice) || 0;
const tradeValue = amount * price;

if (tradeValue >= minTradeSize) {
hasNewTradeToAlert = true;
showToastNotification(trade);
}
}
}
});

if (hasNewTrade && !isFirstLoad) {
if (hasNewTradeToAlert && !isFirstLoad) {
playNotificationSound();
if (typeof refreshTable === "function") {
refreshTable();
}
}

if (!isFirstLoad && typeof refreshTable === "function") {
refreshTable();
}

isFirstLoad = false;
Expand Down
90 changes: 90 additions & 0 deletions IntelliTrader.Web/Views/Home/Settings.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,36 @@
</div>
</div>

<div class="configs mt-4" style="background-color: var(--card-bg) !important; border: 1px solid var(--border-color) !important; color: var(--text-color) !important; padding: 20px; border-radius: 4px; margin-top: 20px;">
<h2>Уведомления и Звук</h2>
<div style="margin-bottom: 15px; display: flex; align-items: center; flex-wrap: wrap;">
<div style="margin-right: 25px; margin-bottom: 10px; display: flex; align-items: center;">
<label style="margin-right: 10px; margin-bottom: 0; font-weight: bold;">Звуковые оповещения:</label>
<input type="checkbox" id="settingsSoundEnabled" style="width: 18px; height: 18px; cursor: pointer;" />
</div>

<div style="margin-right: 25px; margin-bottom: 10px; display: flex; align-items: center;">
<label style="margin-right: 10px; margin-bottom: 0; font-weight: bold;">Громкость:</label>
<input type="range" id="settingsSoundVolume" min="0" max="1" step="0.05" style="width: 120px; cursor: pointer;" />
<span id="settingsSoundVolumeValue" style="margin-left: 8px; font-weight: bold; min-width: 45px;">50%</span>
</div>

<div style="margin-right: 25px; margin-bottom: 10px; display: flex; align-items: center;">
<label style="margin-right: 10px; margin-bottom: 0; font-weight: bold;">Профиль звука:</label>
<select id="settingsSoundProfile" class="form-control form-control-sm" style="width: auto; background-color: var(--input-bg); color: var(--text-color); border: 1px solid var(--input-border); border-radius: 4px;">
<option value="chime">Приятный звон (Chime)</option>
<option value="beep">Высокий писк (Beep)</option>
<option value="arcade">Ретро аркада (Arcade)</option>
</select>
</div>

<div style="margin-bottom: 10px; display: flex; align-items: center;">
<label style="margin-right: 10px; margin-bottom: 0; font-weight: bold;">Мин. размер сделки (USDT):</label>
<input type="number" id="settingsMinTradeSize" min="0" step="1" class="form-control form-control-sm" style="width: 100px; background-color: var(--input-bg); color: var(--text-color); border: 1px solid var(--input-border); border-radius: 4px;" />
</div>
</div>
</div>

<script>
document.addEventListener("DOMContentLoaded", function() {
const select = document.getElementById("settingsThemeSelect");
Expand Down Expand Up @@ -87,6 +117,66 @@
}
});
}

// Notification controls in settings
const soundEnabledCheckbox = document.getElementById("settingsSoundEnabled");
const soundVolumeSlider = document.getElementById("settingsSoundVolume");
const soundVolumeValue = document.getElementById("settingsSoundVolumeValue");
const soundProfileSelect = document.getElementById("settingsSoundProfile");
const minTradeSizeInput = document.getElementById("settingsMinTradeSize");

let isSoundEnabled = localStorage.getItem("realtime_sound_enabled") !== "false";
let soundVolume = localStorage.getItem("realtime_sound_volume") !== null ? parseFloat(localStorage.getItem("realtime_sound_volume")) : 0.5;
let soundProfile = localStorage.getItem("realtime_sound_profile") || "chime";
let minTradeSize = localStorage.getItem("realtime_min_trade_size") !== null ? parseFloat(localStorage.getItem("realtime_min_trade_size")) : 0;

if (soundEnabledCheckbox) soundEnabledCheckbox.checked = isSoundEnabled;
if (soundVolumeSlider) {
soundVolumeSlider.value = soundVolume;
if (soundVolumeValue) soundVolumeValue.textContent = Math.round(soundVolume * 100) + "%";
}
if (soundProfileSelect) soundProfileSelect.value = soundProfile;
if (minTradeSizeInput) minTradeSizeInput.value = minTradeSize;

function dispatchSettingsChange() {
const event = new CustomEvent("notificationSettingsChanged", {
detail: { minTradeSize, soundVolume, soundProfile, isSoundEnabled }
});
document.dispatchEvent(event);
}

if (soundEnabledCheckbox) {
soundEnabledCheckbox.addEventListener("change", function() {
isSoundEnabled = soundEnabledCheckbox.checked;
localStorage.setItem("realtime_sound_enabled", isSoundEnabled);
dispatchSettingsChange();
});
}

if (soundVolumeSlider) {
soundVolumeSlider.addEventListener("input", function() {
soundVolume = parseFloat(soundVolumeSlider.value);
if (soundVolumeValue) soundVolumeValue.textContent = Math.round(soundVolume * 100) + "%";
localStorage.setItem("realtime_sound_volume", soundVolume);
dispatchSettingsChange();
});
}

if (soundProfileSelect) {
soundProfileSelect.addEventListener("change", function() {
soundProfile = soundProfileSelect.value;
localStorage.setItem("realtime_sound_profile", soundProfile);
dispatchSettingsChange();
});
}

if (minTradeSizeInput) {
minTradeSizeInput.addEventListener("input", function() {
minTradeSize = parseFloat(minTradeSizeInput.value) || 0;
localStorage.setItem("realtime_min_trade_size", minTradeSize);
dispatchSettingsChange();
});
}
});
</script>

Expand Down
33 changes: 32 additions & 1 deletion magda_agent_system/agent_tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@
},
{
"id": "web-dashboard-custom-alerts",
"status": "todo",
"status": "done",
"area": "web",
"risk": "low",
"title": "Add custom notification threshold settings on Dashboard",
Expand All @@ -473,6 +473,37 @@
"acceptance": [
"Order caching mechanism is integrated and avoids frequent disk reads for historical trade logs."
]
},
{
"id": "web-dashboard-pair-search-filter",
"status": "todo",
"area": "web",
"risk": "low",
"title": "Add pair search and filter input on Dashboard pairs table",
"description": "Implement a fast search and filter input field on the Web Dashboard pairs table allowing users to quickly filter displayed trading pairs by name or status.",
"allowed_paths": [
"IntelliTrader.Web/Views/**",
"IntelliTrader.Web/Static/**",
"magda_agent_system/agent_tasks.json"
],
"acceptance": [
"Dashboard pairs table filters dynamically as the user types in the search input."
]
},
{
"id": "trading-slippage-tolerance-config",
"status": "todo",
"area": "trading",
"risk": "low",
"title": "Add configurable max slippage tolerance check for buy orders",
"description": "Implement a configurable maximum slippage tolerance percentage check in BuyProcessor to prevent executing orders if price slippage exceeds user tolerance.",
"allowed_paths": [
"IntelliTrader.Trading/Processors/BuyProcessor.cs",
"magda_agent_system/agent_tasks.json"
],
"acceptance": [
"BuyProcessor aborts order placement if current slippage exceeds configured tolerance limit."
]
}
]
}