beast-agent 2.31.0 → 2.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { app, BrowserWindow, WebContentsView, ipcMain, shell, dialog, Tray, Menu, nativeImage, desktopCapturer, session, net: electronNet, clipboard } = require('electron');
3
+ const { app, BrowserWindow, WebContentsView, ipcMain, shell, dialog, Tray, Menu, nativeImage, desktopCapturer, session, net: electronNet, clipboard, Notification } = require('electron');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
  const http = require('http');
@@ -272,6 +272,8 @@ try {
272
272
  try { nodemailer = require('nodemailer'); } catch {}
273
273
 
274
274
  const APP_DIR = path.join(app.getPath('appData'), 'beast');
275
+ /* Windows toast bildirimleri için AppUserModelID erken ayarlanmalı */
276
+ try { app.setAppUserModelId('com.quantumalgo.beastagent'); } catch {}
275
277
  /* mem0-native embedding modeli whisper ile AYNI cache'i kullanır (src/agent/mem0.js okur) */
276
278
  process.env.BEAST_MODELS_DIR = path.join(APP_DIR, 'models');
277
279
 
@@ -301,6 +303,8 @@ for (const d of [APP_DIR, SESSIONS_DIR]) fs.mkdirSync(d, { recursive: true });
301
303
 
302
304
  let win = null;
303
305
  let engine = null;
306
+ /* Paralel ajan durum takibi: bitişte BİR kez toast bildirimi (spam yok) */
307
+ const bgNotifSeen = new Map(); // sid -> son görülen durum
304
308
  let settings = loadSettings();
305
309
  ensureBeastCode();
306
310
  startHealthServer(); /* splash/boot aşamasından itibaren /health ayakta */
@@ -3232,6 +3236,17 @@ function reloadBackend() {
3232
3236
  },
3233
3237
  emit: (ev) => {
3234
3238
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
3239
+ /* PARALEL AJAN BİTİŞİ: running → done/error/aborted geçişinde tek toast */
3240
+ if (ev.type === 'agents' && Array.isArray(ev.jobs)) {
3241
+ for (const j of ev.jobs) {
3242
+ const prev = bgNotifSeen.get(j.id);
3243
+ bgNotifSeen.set(j.id, j.status);
3244
+ if (prev === 'running' && j.status && j.status !== 'running' && j.status !== 'queued' && !j.continuous) {
3245
+ const durum = j.status === 'done' ? 'tamamlandı' : j.status === 'error' ? 'HATA ile bitti' : 'iptal edildi';
3246
+ toastNotify(`Ajan: ${j.title || j.code || 'görev'}`, durum, 'agents');
3247
+ }
3248
+ }
3249
+ }
3235
3250
  /* opencode permission (BC): ask kanalı UI'a zaten yukarıda düştü — WA
3236
3251
  sahibine de tek satır bildir gitsin (/approve · /approve always · /deny) */
3237
3252
  if (ev.type === 'permission.asked' && ev.request && wa && wa.connected) {
@@ -3699,6 +3714,7 @@ app.whenReady().then(() => {
3699
3714
 
3700
3715
  app.on('before-quit', () => {
3701
3716
  app.isQuitting = true;
3717
+ try { finWatchStop(); } catch {} // finance watchdog zamanlayıcısını kapat
3702
3718
  flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
3703
3719
  try { toolsMod.disposeShellSessions(); } catch {} // kalıcı shell oturumlarını kapat
3704
3720
  try { require('./agent/mcp').stopAll(); } catch {} // MCP server süreçlerini kapat
@@ -3741,6 +3757,30 @@ function showWin() {
3741
3757
  }
3742
3758
  }
3743
3759
 
3760
+ /* ---------- Windows toast bildirimi ----------
3761
+ watcher/cron/paralel ajan bitişinde sistem bildirimi; tıklanınca pencere
3762
+ açılır ve renderer'a notify-click eventi gider (ilgili panel açılır).
3763
+ Ayarlardan kapatılabilir: settings.notifyToast === false */
3764
+ function toastNotify(title, body, target) {
3765
+ try {
3766
+ if (settings.notifyToast === false) return;
3767
+ if (typeof Notification === 'undefined' || !Notification.isSupported()) return;
3768
+ const n = new Notification({
3769
+ title: String(title || 'Beast Agent').slice(0, 120),
3770
+ body: String(body || '').slice(0, 240),
3771
+ icon: path.join(__dirname, '..', 'assets', 'tray.png'),
3772
+ silent: false,
3773
+ });
3774
+ n.on('click', () => {
3775
+ showWin();
3776
+ if (win && !win.isDestroyed()) {
3777
+ win.webContents.send('agent:event', { type: 'notify-click', target: String(target || '') });
3778
+ }
3779
+ });
3780
+ n.show();
3781
+ } catch {}
3782
+ }
3783
+
3744
3784
  /* ---------------- dahili tarayıcı ---------------- */
3745
3785
 
3746
3786
  const BROWSER_TOOLBAR_H = 46;
@@ -6875,6 +6915,11 @@ function cronFire(job) {
6875
6915
  const sent = engine.send(sid, {
6876
6916
  text: `[cron: ${job.name}]\n${job.prompt}`,
6877
6917
  });
6918
+ toastNotify(
6919
+ `Cron: ${job.name}`,
6920
+ isReminderJob(job) ? 'Hatırlatma zamanı geldi' : String(job.prompt || '').slice(0, 160),
6921
+ 'cron'
6922
+ );
6878
6923
  if (!sent) cronAnswerPendingDrop(sid); // gönderilemedi — bayat bekleme bırakma
6879
6924
  } catch {}
6880
6925
  cronEmit();
@@ -6892,6 +6937,7 @@ function watcherFire(w, value) {
6892
6937
  w.op === 'changed'
6893
6938
  ? 'izlenen değer değişti'
6894
6939
  : `kural sağlandı (son değer ${value}, koşul ${w.op} ${w.value ?? ''})`;
6940
+ toastNotify(`İzleyici: ${w.name}`, target, 'watchers');
6895
6941
  engine.send(sid, {
6896
6942
  text:
6897
6943
  `[IZLEYICI: ${w.name}] ${target}. ` +
@@ -7426,6 +7472,13 @@ ipcMain.handle('cron:runNow', (_e, id) => {
7426
7472
  cronEmit();
7427
7473
  return r;
7428
7474
  });
7475
+ /* Masaüstü (toast) bildirimleri aç/kapa */
7476
+ ipcMain.handle('notify:get', () => ({ toast: settings.notifyToast !== false }));
7477
+ ipcMain.handle('notify:set', (_e, on) => {
7478
+ settings.notifyToast = !!on;
7479
+ saveSettings();
7480
+ return { toast: settings.notifyToast };
7481
+ });
7429
7482
 
7430
7483
  /* ---------------- tarayıcı IPC ---------------- */
7431
7484
  ipcMain.handle('browser:toggle', () => {
@@ -8365,6 +8418,8 @@ function studioWatchStop() {
8365
8418
  const mt5bridge = require('./mt5bridge');
8366
8419
  const financetools = require('./agent/financetools');
8367
8420
  const customtools = require('./agent/customtools');
8421
+ const finwatch = require('./agent/finwatch');
8422
+ const finstats = require('./agent/finstats');
8368
8423
 
8369
8424
  const financeState = {
8370
8425
  mode: false, /* chat oturumları finance bayrağı alıyor mu */
@@ -8375,6 +8430,20 @@ const financeState = {
8375
8430
  installing: false,
8376
8431
  installTried: false,
8377
8432
  agents: new Map(), /* sid -> { symbols, main, round, timer } — AYNI ANDA koşan finance ajanları */
8433
+ watch: new Map(), /* ticket -> risk otomasyonu durumu (R, BE, kısmi TP...) */
8434
+ watchTimer: null,
8435
+ watchBusy: false,
8436
+ watchTickAt: 0,
8437
+ alerts: [], /* fiyat alarmları (kalıcı: finance/alerts.json) */
8438
+ stats: null, /* son performans özeti (kalıcı: finance/stats.json) */
8439
+ statsAt: 0,
8440
+ lastStatsAt: 0,
8441
+ equity: [], /* equity örnekleri (kalıcı: finance/equity.jsonl) */
8442
+ account: null,
8443
+ dayStart: null,
8444
+ reportCheckAt: 0,
8445
+ flattening: false,
8446
+ breachBusy: false,
8378
8447
  };
8379
8448
 
8380
8449
  function financeDir() {
@@ -8416,6 +8485,30 @@ function finCfg() {
8416
8485
  if (!Number(f.intervalSec)) f.intervalSec = 120;
8417
8486
  if (!Number(f.maxLot)) f.maxLot = 0.1;
8418
8487
  if (f.maxPositions == null) f.maxPositions = 3;
8488
+ /* RİSK OTOMASYONU (watchdog): +R'da BE, trailing, kısmi TP — main süreç
8489
+ saniyelik döngüyle uygular; ajan turunu BEKLEMEZ. 0 = ilgili kural kapalı. */
8490
+ if (typeof f.watchdog !== 'boolean') f.watchdog = true;
8491
+ if (!Number.isFinite(Number(f.beOnR))) f.beOnR = 1;
8492
+ if (!Number.isFinite(Number(f.beOffsetR))) f.beOffsetR = 0.05;
8493
+ if (!Number.isFinite(Number(f.trailStartR))) f.trailStartR = 1.5;
8494
+ if (!Number.isFinite(Number(f.trailR))) f.trailR = 0.5;
8495
+ if (!Number.isFinite(Number(f.partialR))) f.partialR = 0;
8496
+ if (!Number.isFinite(Number(f.partialPct))) f.partialPct = 50;
8497
+ /* BİLDİRİM: işlem/koruma/alarm olayları bağlı kanallara (TG/WA/Discord)
8498
+ gider; 'auto' = bağlı olanlar, 'off' = yalnız panel/masaüstü */
8499
+ if (typeof f.notifyTarget !== 'string') f.notifyTarget = 'auto';
8500
+ if (typeof f.notifyTrades !== 'boolean') f.notifyTrades = true;
8501
+ if (typeof f.notifyWatchdog !== 'boolean') f.notifyWatchdog = true;
8502
+ if (!Number.isFinite(Number(f.maxDailyLossPct))) f.maxDailyLossPct = 3;
8503
+ /* GÜNLÜK LİMİT AKSİYONU: warn = yalnız uyarı; stop = tüm finance ajanlarını
8504
+ durdur; flatten = durdur + tüm pozisyonları kapat */
8505
+ if (!['warn', 'stop', 'flatten'].includes(String(f.dailyLossAction))) f.dailyLossAction = 'stop';
8506
+ /* İŞLEM ÖNCESİ RİSK KATMANI: %risk lot + yoğunluk + marj kalkanı */
8507
+ if (!Number.isFinite(Number(f.riskPerTradePct))) f.riskPerTradePct = 1;
8508
+ if (!Number.isFinite(Number(f.maxPerSymbol))) f.maxPerSymbol = 2;
8509
+ if (!Number.isFinite(Number(f.maxSameSide))) f.maxSameSide = 3;
8510
+ if (!Number.isFinite(Number(f.minMarginLevel))) f.minMarginLevel = 150;
8511
+ if (f.weeklyReport == null) f.weeklyReport = true;
8419
8512
  return f;
8420
8513
  }
8421
8514
 
@@ -8451,21 +8544,605 @@ function finPush(fn, data) {
8451
8544
  } catch {}
8452
8545
  }
8453
8546
 
8454
- /* financetools ayar + bildirim kancası */
8547
+ /* ---------- FINANCE KALICILIK + BİLDİRİM + RİSK OTOMASYONU ---------- */
8548
+
8549
+ function finFile(name) {
8550
+ return path.join(financeDir(), String(name || ''));
8551
+ }
8552
+
8553
+ function finReadJson(file, def) {
8554
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return def; }
8555
+ }
8556
+
8557
+ function finWriteJson(file, obj) {
8558
+ try {
8559
+ fs.mkdirSync(path.dirname(file), { recursive: true });
8560
+ fs.writeFileSync(file, JSON.stringify(obj, null, 2));
8561
+ } catch {}
8562
+ }
8563
+
8564
+ /* İşlem günlüğü: her karar (gerekçe notu, açılış, koruma, alarm, kapanış)
8565
+ finance/journal.jsonl'a düşer; haftalık rapor buradan beslenir. */
8566
+ function finJournal(entry) {
8567
+ const e = { at: Date.now(), ...entry };
8568
+ try {
8569
+ const p = finFile('journal.jsonl');
8570
+ try {
8571
+ if (fs.statSync(p).size > 3 * 1024 * 1024) fs.renameSync(p, finFile('journal-old.jsonl'));
8572
+ } catch {}
8573
+ fs.appendFileSync(p, JSON.stringify(e) + '\n');
8574
+ } catch {}
8575
+ return e;
8576
+ }
8577
+
8578
+ function finJournalTail(n) {
8579
+ try {
8580
+ const lines = fs.readFileSync(finFile('journal.jsonl'), 'utf8').trim().split('\n');
8581
+ return lines
8582
+ .slice(-Math.max(1, Math.min(500, Number(n) || 20)))
8583
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } })
8584
+ .filter(Boolean);
8585
+ } catch {
8586
+ return [];
8587
+ }
8588
+ }
8589
+
8590
+ function finAgentInfo(sid) {
8591
+ try {
8592
+ const a = financeState.agents.get(String(sid || ''));
8593
+ if (a) {
8594
+ const role = String(a.role || '');
8595
+ const def = role ? finRoleDef(role) : null;
8596
+ return { main: !!a.main, role: role || (a.main ? 'trader' : 'worker'), label: def ? def.label : a.main ? 'Trader' : 'Sembol Ajanı' };
8597
+ }
8598
+ /* işçi kaydı yoksa (chat copilot'ı vb.) oturumdan başlık/rol oku */
8599
+ if (sid && engine) {
8600
+ const s = engine.cache.get(String(sid)) || engine._load(String(sid));
8601
+ if (s && s.finance) {
8602
+ const def = s.financeRole ? finRoleDef(s.financeRole) : null;
8603
+ return { main: false, role: String(s.financeRole || 'chat'), label: def ? def.label : s.bgTitle ? String(s.bgTitle) : 'Finance Chat' };
8604
+ }
8605
+ }
8606
+ } catch {}
8607
+ return {};
8608
+ }
8609
+
8610
+ /* Bildirim: panel/masaüstü toast + istenen kanal (Telegram/WhatsApp/Discord).
8611
+ Kanal seçimi finCfg().notifyTarget: auto|whatsapp|telegram|discord|off
8612
+ panel=false → panel toast'ı atla (çağıran zaten finPush('trade') yaptıysa). */
8613
+ function financeNotify(text, kind, panel) {
8614
+ const line = String(text || '').trim();
8615
+ if (!line) return;
8616
+ if (panel !== false) finPush('notify', { line, kind: kind || '' });
8617
+ let cfg;
8618
+ try { cfg = finCfg(); } catch { cfg = {}; }
8619
+ const target = String((cfg && cfg.notifyTarget) || 'auto');
8620
+ if (target === 'off') return;
8621
+ const body = '💼 *Beast Finance*\n' + line;
8622
+ const senders = [];
8623
+ try {
8624
+ if ((target === 'whatsapp' || target === 'auto') && wa && wa.connected) {
8625
+ const own = waOwnerNum();
8626
+ if (own) {
8627
+ const jid = own + '@s.whatsapp.net';
8628
+ senders.push(() => sendWaSafe(jid, body));
8629
+ }
8630
+ }
8631
+ } catch {}
8632
+ try {
8633
+ if ((target === 'telegram' || target === 'auto') && tg && tg.connected) {
8634
+ for (const id of tgOwnerIds()) senders.push(() => sendTgSafe(id, body));
8635
+ }
8636
+ } catch {}
8637
+ try {
8638
+ if ((target === 'discord' || target === 'auto') && dc && dc.connected) {
8639
+ for (const id of dcOwnerIds()) senders.push(() => sendDcSafe(id, body));
8640
+ }
8641
+ } catch {}
8642
+ for (const fn of senders) {
8643
+ try { Promise.resolve(fn()).catch(() => {}); } catch {}
8644
+ }
8645
+ }
8646
+
8647
+ /* ---- fiyat alarmları (kalıcı) ---- */
8648
+ function finAlertsLoad() {
8649
+ const raw = finReadJson(finFile('alerts.json'), []);
8650
+ financeState.alerts = (Array.isArray(raw) ? raw : [])
8651
+ .map((a) => ({
8652
+ id: String(a && a.id ? a.id : ''),
8653
+ symbol: String((a && a.symbol) || '').toUpperCase(),
8654
+ price: Number(a && a.price) || 0,
8655
+ direction: String((a && a.direction) || 'above').toLowerCase(),
8656
+ note: String((a && a.note) || '').slice(0, 200),
8657
+ at: Number(a && a.at) || Date.now(),
8658
+ }))
8659
+ .filter((a) => a.id && a.symbol && a.price > 0);
8660
+ }
8661
+
8662
+ function finAlertsSave() {
8663
+ finWriteJson(finFile('alerts.json'), financeState.alerts);
8664
+ }
8665
+
8666
+ function finAlertApi() {
8667
+ return {
8668
+ list: () => financeState.alerts.slice(),
8669
+ set: (a) => {
8670
+ const symbol = String((a && a.symbol) || '').toUpperCase();
8671
+ const price = Number(a && a.price);
8672
+ if (!symbol || !(price > 0)) return null;
8673
+ const alarm = {
8674
+ id: 'a' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
8675
+ symbol,
8676
+ price,
8677
+ direction: String((a && a.direction) || 'above').toLowerCase() === 'below' ? 'below' : 'above',
8678
+ note: String((a && a.note) || '').slice(0, 200),
8679
+ at: Date.now(),
8680
+ };
8681
+ financeState.alerts.push(alarm);
8682
+ finAlertsSave();
8683
+ financeLog(`[alarm] kuruldu: ${alarm.symbol} ${alarm.direction === 'below' ? '≤' : '≥'} ${alarm.price}`);
8684
+ return alarm;
8685
+ },
8686
+ remove: (id) => {
8687
+ const before = financeState.alerts.length;
8688
+ financeState.alerts = financeState.alerts.filter((a) => a.id !== String(id));
8689
+ if (financeState.alerts.length !== before) {
8690
+ finAlertsSave();
8691
+ financeLog('[alarm] silindi: ' + id);
8692
+ return true;
8693
+ }
8694
+ return false;
8695
+ },
8696
+ };
8697
+ }
8698
+
8699
+ /* ---- equity serisi + günlük kayıp uyarısı ---- */
8700
+ function finEquityLoad() {
8701
+ if (financeState.equity && financeState.equity.length) return financeState.equity;
8702
+ try {
8703
+ const lines = fs.readFileSync(finFile('equity.jsonl'), 'utf8').trim().split('\n');
8704
+ financeState.equity = lines
8705
+ .slice(-4000)
8706
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } })
8707
+ .filter((p) => p && isFinite(Number(p.equity)));
8708
+ } catch {
8709
+ financeState.equity = [];
8710
+ }
8711
+ return financeState.equity;
8712
+ }
8713
+
8714
+ function finEquitySample(account) {
8715
+ const eq = Number(account && account.equity);
8716
+ if (!isFinite(eq)) return;
8717
+ finEquityLoad();
8718
+ const last = financeState.equity.length ? financeState.equity[financeState.equity.length - 1] : null;
8719
+ const now = Date.now();
8720
+ if (last && now - Number(last.at) < 30000) return;
8721
+ const point = { at: now, equity: eq, balance: Number(account.balance) || 0 };
8722
+ financeState.equity.push(point);
8723
+ try {
8724
+ fs.appendFileSync(finFile('equity.jsonl'), JSON.stringify(point) + '\n');
8725
+ if (financeState.equity.length > 4000) {
8726
+ financeState.equity = financeState.equity.slice(-2500);
8727
+ fs.writeFileSync(finFile('equity.jsonl'), financeState.equity.map((p) => JSON.stringify(p)).join('\n') + '\n');
8728
+ }
8729
+ } catch {}
8730
+ finDailyLossCheck(point);
8731
+ }
8732
+
8733
+ function finDailyLossCheck(point) {
8734
+ const cfg = finCfg();
8735
+ const day = finstats.dayKey(point.at);
8736
+ let ds = financeState.dayStart;
8737
+ if (!ds || ds.day !== day) {
8738
+ financeState.dayStart = ds = { day, equity: Number(point.balance) > 0 ? Number(point.balance) : point.equity, warned: false, acted: false };
8739
+ }
8740
+ if (ds.warned || !(Number(cfg.maxDailyLossPct) > 0) || !(ds.equity > 0)) return;
8741
+ const dd = ((ds.equity - point.equity) / ds.equity) * 100;
8742
+ if (dd >= Number(cfg.maxDailyLossPct)) {
8743
+ ds.warned = true;
8744
+ const action = String(cfg.dailyLossAction || 'stop');
8745
+ const tail = action === 'flatten' ? ' — ajanlar durduruldu, pozisyonlar kapatılıyor' : action === 'stop' ? ' — ajanlar durduruldu' : '';
8746
+ const line = `⚠️ Günlük kayıp %${dd.toFixed(2)} (limit %${cfg.maxDailyLossPct})${tail}`;
8747
+ financeLog('[risk] ' + line);
8748
+ financeNotify(line, 'drawdown');
8749
+ finJournal({ kind: 'drawdown', pct: Math.round(dd * 100) / 100, action });
8750
+ if (action === 'stop' || action === 'flatten') {
8751
+ finDailyLossBreach(action).catch(() => {});
8752
+ }
8753
+ }
8754
+ }
8755
+
8756
+ /* Günlük limit aşımı: ajanları durdur (+ istenirse tüm pozisyonları kapat).
8757
+ Ajanlar önce durur ki flatten sırasında yeni işlem açılmasın. */
8758
+ async function finDailyLossBreach(action) {
8759
+ if (financeState.breachBusy) return;
8760
+ financeState.breachBusy = true;
8761
+ try {
8762
+ await finStopAllFinanceAgents('günlük kayıp limiti aşıldı — risk katmanı durdurdu');
8763
+ const stopLine = '🛑 Risk limiti: tüm finance ajanları durduruldu';
8764
+ financeLog('[risk] ' + stopLine);
8765
+ financeNotify(stopLine, 'risk');
8766
+ finJournal({ kind: 'risk-stop', action });
8767
+ if (action === 'flatten') await finFlattenAll();
8768
+ } finally {
8769
+ financeState.breachBusy = false;
8770
+ }
8771
+ }
8772
+
8773
+ async function finStopAllFinanceAgents(reason) {
8774
+ financeState.traderOn = false;
8775
+ const sids = [...financeState.agents.keys()];
8776
+ for (const sid of sids) {
8777
+ try { finAgentStop(String(sid), reason); } catch {}
8778
+ }
8779
+ try { finPush('trader', { state: 'stopped', reason }); } catch {}
8780
+ try { financeLog('[risk] finance ajanları durduruldu: ' + sids.length + ' ajan'); } catch {}
8781
+ return sids.length;
8782
+ }
8783
+
8784
+ /* Tüm açık pozisyonları kapatır (günlük limit 'flatten' aksiyonu). */
8785
+ async function finFlattenAll() {
8786
+ if (financeState.flattening) return 0;
8787
+ if (!mt5bridge.running) return 0;
8788
+ financeState.flattening = true;
8789
+ let closed = 0;
8790
+ try {
8791
+ const r = await mt5bridge.call('positions', {}, 10000);
8792
+ const list = (r && r.ok && r.data && r.data.positions) || [];
8793
+ for (const p of list) {
8794
+ const cr = await mt5bridge.call('close', { ticket: p.ticket, volume: 0 }, 15000).catch(() => null);
8795
+ if (cr && cr.ok) closed++;
8796
+ }
8797
+ const line = `🚨 Günlük limit: ${closed}/${list.length} pozisyon kapatıldı`;
8798
+ financeLog('[risk] ' + line);
8799
+ financeNotify(line, 'risk');
8800
+ finJournal({ kind: 'risk-flatten', closed, total: list.length });
8801
+ finStatsRefresh(true).catch(() => {});
8802
+ } finally {
8803
+ financeState.flattening = false;
8804
+ }
8805
+ return closed;
8806
+ }
8807
+
8808
+ /* ---- performans istatistikleri (kalıcı) ---- */
8809
+ function finStatsLoad() {
8810
+ const s = finReadJson(finFile('stats.json'), null);
8811
+ if (s && typeof s === 'object') financeState.stats = s;
8812
+ }
8813
+
8814
+ async function finStatsRefresh(force) {
8815
+ if (!mt5bridge.running) return financeState.stats;
8816
+ const now = Date.now();
8817
+ if (!force && financeState.statsAt && now - financeState.statsAt < 60000) return financeState.stats;
8818
+ financeState.statsAt = now;
8819
+ try {
8820
+ const r = await mt5bridge.call('deals', { days: 90 }, 15000);
8821
+ const deals = (r && r.ok && r.data && r.data.deals) || [];
8822
+ const stats = finstats.summarizeDeals(deals);
8823
+ stats.drawdown = finstats.maxDrawdown(finEquityLoad());
8824
+ financeState.stats = stats;
8825
+ finWriteJson(finFile('stats.json'), stats);
8826
+ return stats;
8827
+ } catch {
8828
+ return financeState.stats;
8829
+ }
8830
+ }
8831
+
8832
+ /* ---- işlem günlüğü (görülen/kapanan pozisyonlar) ---- */
8833
+ async function finRecordClose(ticket, st) {
8834
+ let net = null;
8835
+ let symbol = (st && st.symbol) || '';
8836
+ try {
8837
+ const r = await mt5bridge.call('deals', { days: 3 }, 12000);
8838
+ const deals = (r && r.ok && r.data && r.data.deals) || [];
8839
+ const mine = deals.filter((d) => String(d.position_id) === String(ticket));
8840
+ if (mine.length) {
8841
+ net = 0;
8842
+ for (const d of mine) net += (Number(d.profit) || 0) + (Number(d.swap) || 0) + (Number(d.commission) || 0);
8843
+ symbol = String(mine[0].symbol || symbol);
8844
+ }
8845
+ } catch {}
8846
+ const rounded = net == null ? null : Math.round(net * 100) / 100;
8847
+ finJournal({ kind: 'close', ticket, symbol, net: rounded, side: (st && st.side) || '' });
8848
+ const pl = rounded == null ? '' : ` · K/Z ${rounded >= 0 ? '+' : ''}${rounded.toFixed(2)}`;
8849
+ const line = `🏁 Pozisyon kapandı: ${symbol || '?'} #${ticket}${pl}`;
8850
+ financeLog('[watchdog] ' + line);
8851
+ if (finCfg().notifyTrades !== false) financeNotify(line, 'close');
8852
+ if (rounded != null) finStatsRefresh(true);
8853
+ }
8854
+
8855
+ /* ---- risk otomasyonu (watchdog) ---- */
8856
+ const finSymMetaCache = new Map(); /* symbol -> { at, row } */
8857
+
8858
+ async function finSymbolMetaGet(symbol) {
8859
+ const sym = String(symbol || '');
8860
+ if (!sym) return null;
8861
+ const hit = finSymMetaCache.get(sym);
8862
+ if (hit && Date.now() - hit.at < 60000) return hit.row;
8863
+ try {
8864
+ const r = await mt5bridge.call('symbols', { symbols: [sym] }, 10000);
8865
+ const row = r && r.ok && r.data && r.data.symbols && r.data.symbols[0];
8866
+ if (row && !row.missing) {
8867
+ finSymMetaCache.set(sym, { at: Date.now(), row });
8868
+ return row;
8869
+ }
8870
+ } catch {}
8871
+ return hit ? hit.row : null;
8872
+ }
8873
+
8874
+ async function finCheckAlerts() {
8875
+ const alerts = financeState.alerts || [];
8876
+ if (!alerts.length) return;
8877
+ const syms = [...new Set(alerts.map((a) => a.symbol))];
8878
+ let rows = [];
8879
+ try {
8880
+ const r = await mt5bridge.call('symbols', { symbols: syms }, 10000);
8881
+ if (r && r.ok) rows = (r.data && r.data.symbols) || [];
8882
+ } catch {}
8883
+ const prices = {};
8884
+ for (const row of rows) {
8885
+ if (!row || row.missing) continue;
8886
+ prices[String(row.symbol)] = { bid: Number(row.bid), ask: Number(row.ask) };
8887
+ }
8888
+ let changed = false;
8889
+ for (const a of alerts.slice()) {
8890
+ const p = prices[a.symbol];
8891
+ if (!p || !isFinite(p.bid)) continue;
8892
+ const hit = a.direction === 'below' ? p.bid <= a.price : p.bid >= a.price;
8893
+ if (!hit) continue;
8894
+ changed = true;
8895
+ financeState.alerts = financeState.alerts.filter((x) => x.id !== a.id);
8896
+ const line = `🔔 ALARM: ${a.symbol} ${a.direction === 'below' ? '≤' : '≥'} ${a.price} (şimdi ${p.bid})${a.note ? ' — ' + a.note : ''}`;
8897
+ financeLog('[alarm] ' + line);
8898
+ financeNotify(line, 'alert');
8899
+ finJournal({ kind: 'alert', symbol: a.symbol, price: a.price, direction: a.direction, note: a.note || '' });
8900
+ }
8901
+ if (changed) finAlertsSave();
8902
+ }
8903
+
8904
+ async function finWatchTick() {
8905
+ if (!mt5bridge.running || financeState.watchBusy) return;
8906
+ financeState.watchBusy = true;
8907
+ try {
8908
+ const cfg = finCfg();
8909
+ financeState.watchTickAt = Date.now();
8910
+ const [posR, accR] = await Promise.all([
8911
+ mt5bridge.call('positions', {}, 8000).catch(() => null),
8912
+ mt5bridge.call('account', {}, 8000).catch(() => null),
8913
+ ]);
8914
+ const account = accR && accR.ok ? (accR.data && accR.data.account) : null;
8915
+ const positionsOk = !!(posR && posR.ok);
8916
+ const positions = positionsOk ? ((posR.data && posR.data.positions) || []) : [];
8917
+ if (account) {
8918
+ financeState.account = account;
8919
+ finEquitySample(account);
8920
+ }
8921
+ const live = new Set();
8922
+ for (const p of positions) {
8923
+ const ticket = String(p.ticket);
8924
+ live.add(ticket);
8925
+ let st = financeState.watch.get(ticket);
8926
+ if (!st) {
8927
+ st = {
8928
+ symbol: String(p.symbol || ''),
8929
+ side: Number(p.type) === 0 ? 'buy' : 'sell',
8930
+ entry: Number(p.price_open) || 0,
8931
+ r: 0,
8932
+ be: false,
8933
+ partial: false,
8934
+ warnNoSL: false,
8935
+ warnSL: false,
8936
+ seenAt: Date.now(),
8937
+ };
8938
+ financeState.watch.set(ticket, st);
8939
+ const isBeast = Number(p.magic) === 20260908 || /beast/i.test(String(p.comment || ''));
8940
+ if (isBeast) {
8941
+ finJournal({ kind: 'open', ticket, symbol: st.symbol, side: st.side, volume: Number(p.volume) || 0, entry: st.entry, sl: Number(p.sl) || 0, tp: Number(p.tp) || 0 });
8942
+ }
8943
+ finWatchSave();
8944
+ }
8945
+ if (cfg.watchdog === false) continue;
8946
+ let meta = null;
8947
+ try { meta = await finSymbolMetaGet(p.symbol); } catch {}
8948
+ if (!meta) continue;
8949
+ let decision;
8950
+ try { decision = finwatch.plan(p, meta, st, cfg); } catch { continue; }
8951
+ if (decision.r > 0 && !(st.r > 0)) {
8952
+ st.r = decision.r;
8953
+ finWatchSave();
8954
+ }
8955
+ for (const act of decision.actions) {
8956
+ if (act.kind === 'modify') {
8957
+ const r = await mt5bridge.call('modify', { ticket: p.ticket, sl: act.sl, tp: Number(p.tp) || 0 }, 15000).catch(() => null);
8958
+ if (r && r.ok) {
8959
+ st.be = true;
8960
+ const line = `🛡️ SL taşındı: ${p.symbol} #${ticket} → ${act.sl}`;
8961
+ financeLog('[watchdog] ' + line);
8962
+ if (cfg.notifyWatchdog !== false) financeNotify(line, 'watchdog');
8963
+ finJournal({ kind: 'watchdog', action: 'sl-move', ticket, symbol: p.symbol, sl: act.sl });
8964
+ }
8965
+ } else if (act.kind === 'partial') {
8966
+ const r = await mt5bridge.call('close', { ticket: p.ticket, volume: act.volume }, 15000).catch(() => null);
8967
+ if (r && r.ok) {
8968
+ st.partial = true;
8969
+ const line = `🎯 Kısmi TP: ${p.symbol} #${ticket} ${act.volume} lot kapatıldı`;
8970
+ financeLog('[watchdog] ' + line);
8971
+ if (cfg.notifyWatchdog !== false) financeNotify(line, 'watchdog');
8972
+ finJournal({ kind: 'watchdog', action: 'partial-tp', ticket, symbol: p.symbol, volume: act.volume });
8973
+ finStatsRefresh(true);
8974
+ }
8975
+ } else if (act.kind === 'warnSL') {
8976
+ st.warnSL = true;
8977
+ const line = `⚠️ SL yaklaşıyor: ${p.symbol} #${ticket} (SL ${p.sl}, fiyat ${p.price_current})`;
8978
+ financeLog('[watchdog] ' + line);
8979
+ if (cfg.notifyWatchdog !== false) financeNotify(line, 'watchdog');
8980
+ finJournal({ kind: 'watchdog', action: 'sl-near', ticket, symbol: p.symbol, sl: Number(p.sl) || 0 });
8981
+ } else if (act.kind === 'warnNoSL') {
8982
+ st.warnNoSL = true;
8983
+ const line = `⚠️ SL'siz pozisyon: ${p.symbol} #${ticket} — risk koruması yok`;
8984
+ financeLog('[watchdog] ' + line);
8985
+ if (cfg.notifyWatchdog !== false) financeNotify(line, 'watchdog');
8986
+ }
8987
+ }
8988
+ if (decision.rearmSL && st.warnSL) st.warnSL = false;
8989
+ }
8990
+ /* kapanan pozisyonlar: net K/Z ile günlük + bildirim (yalnız pozisyon
8991
+ listesi GERÇEKTEN alındıysa — köprü kesintisinde yanlış kapanış yok) */
8992
+ if (positionsOk) {
8993
+ for (const [ticket, st] of [...financeState.watch]) {
8994
+ if (live.has(ticket)) continue;
8995
+ financeState.watch.delete(ticket);
8996
+ finWatchSave();
8997
+ try { await finRecordClose(ticket, st); } catch {}
8998
+ }
8999
+ }
9000
+ try { await finCheckAlerts(); } catch {}
9001
+ if (!financeState.lastStatsAt || Date.now() - financeState.lastStatsAt > 120000) {
9002
+ financeState.lastStatsAt = Date.now();
9003
+ finStatsRefresh(true).catch(() => {});
9004
+ }
9005
+ finMaybeWeeklyReport();
9006
+ } catch {
9007
+ } finally {
9008
+ financeState.watchBusy = false;
9009
+ }
9010
+ }
9011
+
9012
+ function finWatchStart() {
9013
+ if (financeState.watchTimer) return;
9014
+ financeState.watchTimer = setInterval(() => { finWatchTick().catch(() => {}); }, 5000);
9015
+ finWatchTick().catch(() => {});
9016
+ }
9017
+
9018
+ function finWatchStop() {
9019
+ if (financeState.watchTimer) {
9020
+ clearInterval(financeState.watchTimer);
9021
+ financeState.watchTimer = null;
9022
+ }
9023
+ }
9024
+
9025
+ function finWatchLoad() {
9026
+ const raw = finReadJson(finFile('watchdog.json'), {});
9027
+ try {
9028
+ for (const [k, v] of Object.entries(raw || {})) {
9029
+ if (!v || typeof v !== 'object') continue;
9030
+ financeState.watch.set(String(k), v);
9031
+ }
9032
+ } catch {}
9033
+ }
9034
+
9035
+ function finWatchSave() {
9036
+ try { finWriteJson(finFile('watchdog.json'), Object.fromEntries(financeState.watch)); } catch {}
9037
+ }
9038
+
9039
+ /* ---- haftalık rapor (ajan değerlendirmesi + istatistik + günlük) ---- */
9040
+ async function finWeeklyReport(manual) {
9041
+ const cfg = finCfg();
9042
+ const now = Date.now();
9043
+ if (!manual && cfg.weeklyReport === false) return null;
9044
+ if (!manual && Number(cfg.lastReportAt) && now - Number(cfg.lastReportAt) < 7 * 86400000) return null;
9045
+ let stats = financeState.stats;
9046
+ try { stats = (await finStatsRefresh(true)) || stats; } catch {}
9047
+ let account = financeState.account;
9048
+ const [accR, posR] = await Promise.all([
9049
+ account ? Promise.resolve(null) : mt5bridge.call('account', {}, 8000).catch(() => null),
9050
+ mt5bridge.call('positions', {}, 8000).catch(() => null),
9051
+ ]);
9052
+ if (accR && accR.ok) account = accR.data && accR.data.account;
9053
+ const positions = posR && posR.ok ? ((posR.data && posR.data.positions) || []) : [];
9054
+ const fromTs = now - 7 * 86400000;
9055
+ const equity = (financeState.equity || []).filter((p) => Number(p.at) >= fromTs - 86400000);
9056
+ const journal = finJournalTail(40);
9057
+ const drawdown = finstats.maxDrawdown(equity);
9058
+ let review = '';
9059
+ if (engine) {
9060
+ try {
9061
+ const task =
9062
+ 'Beast Finance haftalık performans değerlendirmesi: aşağıdaki istatistik ve işlem günlüğünü ELEŞTİREL yorumla. ' +
9063
+ 'Ne iyi gitti, hangi hatalar tekrarlandı, gelecek hafta ne değişmeli? En fazla 12 satır, madde madde, Türkçe.';
9064
+ const ctx = JSON.stringify({ stats, drawdown, journal: journal.slice(-20) }).slice(0, 12000);
9065
+ const signal = typeof AbortSignal !== 'undefined' && AbortSignal.timeout ? AbortSignal.timeout(120000) : undefined;
9066
+ const res = await engine._subagent(task, ctx, signal, null, '');
9067
+ review = String(res || '').trim().slice(0, 4000);
9068
+ } catch {}
9069
+ }
9070
+ const md = finstats.buildWeeklyReport({ stats, drawdown, equity, journal, account, positions, fromTs, toTs: now, review });
9071
+ const day = finstats.dayKey(now);
9072
+ const file = path.join(financeDir(), 'reports', 'weekly-' + day + '.md');
9073
+ try {
9074
+ fs.mkdirSync(path.dirname(file), { recursive: true });
9075
+ fs.writeFileSync(file, md);
9076
+ } catch {}
9077
+ cfg.lastReportAt = now;
9078
+ try { saveSettings(); } catch {}
9079
+ const n = stats ? (Number(stats.netProfit) >= 0 ? '+' : '') + stats.netProfit : '?';
9080
+ const line = `📊 Haftalık rapor hazır (net ${n}) — ${path.basename(file)}`;
9081
+ financeLog('[rapor] ' + line);
9082
+ financeNotify(line, 'report');
9083
+ return { ok: true, path: file, text: md, stats };
9084
+ }
9085
+
9086
+ function finMaybeWeeklyReport() {
9087
+ try {
9088
+ const cfg = finCfg();
9089
+ if (cfg.weeklyReport === false) return;
9090
+ if (!Number(cfg.lastReportAt)) {
9091
+ /* ilk kurulum: temel al — rapor 7 gün sonra üretilir (boş rapor atma) */
9092
+ cfg.lastReportAt = Date.now();
9093
+ try { saveSettings(); } catch {}
9094
+ return;
9095
+ }
9096
+ if (Date.now() - Number(cfg.lastReportAt) < 7 * 86400000) return;
9097
+ if (Date.now() - Number(financeState.reportCheckAt || 0) < 3600000) return;
9098
+ financeState.reportCheckAt = Date.now();
9099
+ finWeeklyReport(false).catch(() => {});
9100
+ } catch {}
9101
+ }
9102
+
9103
+ try { finAlertsLoad(); } catch {}
9104
+ try { finWatchLoad(); } catch {}
9105
+ try { finStatsLoad(); } catch {}
9106
+ try { finEquityLoad(); } catch {}
9107
+
9108
+ /* financetools ayar + bildirim kancası: her olay kalıcı günlüğe + bildirime */
8455
9109
  try {
8456
9110
  financetools.setConfig(() => finCfg());
9111
+ financetools.setAlerts(finAlertApi());
8457
9112
  financetools.setNotify((entry) => {
8458
9113
  const k = String((entry && entry.kind) || '');
8459
9114
  const d = entry && entry.data ? entry.data : {};
9115
+ const ai = finAgentInfo(entry && entry.sid);
9116
+ const who = ai.label || '';
9117
+ const sid = String((entry && entry.sid) || '');
9118
+ if (k === 'note') {
9119
+ finJournal({ kind: 'note', sid, agent: who, symbol: d.symbol, note: d.note, ticket: d.ticket || 0, side: d.side || '' });
9120
+ return;
9121
+ }
9122
+ if (k === 'trade') {
9123
+ finJournal({ kind: 'trade', sid, agent: who, symbol: d.symbol, side: d.side, volume: d.volume, sl: d.sl, tp: d.tp, reason: d.comment || '' });
9124
+ } else if (k === 'close') {
9125
+ finJournal({ kind: 'close-req', sid, agent: who, ticket: d.ticket, volume: d.volume });
9126
+ return; /* kesin kapanış K/Z'si watchdog tarafından yazılır (çift bildirim yok) */
9127
+ } else if (k === 'modify') {
9128
+ finJournal({ kind: 'modify', sid, agent: who, ticket: d.ticket, sl: d.sl, tp: d.tp });
9129
+ } else if (k === 'pending') {
9130
+ finJournal({ kind: 'pending', sid, agent: who, symbol: d.symbol, type: d.type, volume: d.volume });
9131
+ } else if (k === 'cancel') {
9132
+ finJournal({ kind: 'cancel', sid, agent: who, ticket: d.ticket });
9133
+ }
8460
9134
  let line = '';
8461
- if (k === 'trade') line = `İŞLEM AÇILDI: ${d.side} ${d.volume} ${d.symbol}`;
8462
- else if (k === 'close') line = `POZİSYON KAPANDI: ticket ${d.ticket}${d.volume ? ' (' + d.volume + ' lot)' : ''}`;
8463
- else if (k === 'modify') line = `SL/TP GÜNCELLENDİ: ticket ${d.ticket} (SL ${d.sl || 0} / TP ${d.tp || 0})`;
8464
- else if (k === 'pending') line = `BEKLEYEN EMİR: ${d.type} ${d.volume} ${d.symbol}`;
9135
+ if (k === 'trade') line = `İŞLEM AÇILDI: ${d.side} ${d.volume} ${d.symbol}${who ? ' · ' + who : ''}`;
9136
+ else if (k === 'modify') line = `SL/TP GÜNCELLENDİ: ticket ${d.ticket} (SL ${d.sl || 0} / TP ${d.tp || 0})${who ? ' · ' + who : ''}`;
9137
+ else if (k === 'pending') line = `BEKLEYEN EMİR: ${d.type} ${d.volume} ${d.symbol}${who ? ' · ' + who : ''}`;
8465
9138
  else if (k === 'cancel') line = `EMİR İPTAL: ticket ${d.ticket}`;
8466
9139
  if (line) {
8467
9140
  financeLog('[ajan] ' + line);
8468
9141
  finPush('trade', { line });
9142
+ if (finCfg().notifyTrades !== false && (k === 'trade' || k === 'pending' || k === 'cancel')) {
9143
+ financeNotify(line, k, false); /* panel toast'ı yukarıda — kanallara gönder */
9144
+ }
9145
+ if (k === 'trade') finStatsRefresh(true).catch(() => {});
8469
9146
  }
8470
9147
  });
8471
9148
  } catch {}
@@ -8508,6 +9185,7 @@ function financeEnsureBridge() {
8508
9185
  candidates,
8509
9186
  terminal: String(f.terminalPath || '').trim(),
8510
9187
  });
9188
+ finWatchStart(); /* risk otomasyonu + bildirimler köprü açılırken başlar */
8511
9189
  return mt5bridge.status();
8512
9190
  }
8513
9191
 
@@ -8545,6 +9223,8 @@ mt5bridge.on('bridge', (m) => {
8545
9223
  if (m && m.connected) {
8546
9224
  financeLog('[MT5] terminal bağlı: ' + ((m.terminal && (m.terminal.name + ' @ ' + m.terminal.company)) || 'MT5'));
8547
9225
  if (m.account) financeLog(`[MT5] hesap ${m.account.login} · bakiye ${m.account.balance} ${m.account.currency}`);
9226
+ finWatchStart();
9227
+ finStatsRefresh(true).catch(() => {});
8548
9228
  } else if (m && m.error) {
8549
9229
  financeLog('[MT5] bağlantı yok — ' + m.error);
8550
9230
  if (/paket/i.test(m.error)) financeTryInstall();
@@ -8713,12 +9393,14 @@ function finTraderBrief(agent) {
8713
9393
  `Tur aralığı: ${f.intervalSec} sn · Max lot: ${f.maxLot} · Max eşzamanlı pozisyon: ${f.maxPositions}`,
8714
9394
  roleDef
8715
9395
  ? `UZMANLIK: ${roleDef.desc} — raporlarını bu çerçevede yaz; İŞLEM AÇMA, yalnız analiz + net öneri üret.`
8716
- : 'Otomatik işlem AÇIK (daima): mt5_trade/mt5_close/mt5_modify/mt5_pending kullanabilirsin (limitler sistemce zorlanır).',
9396
+ : 'Otomatik işlem AÇIK (daima): mt5_trade/mt5_close/mt5_modify/mt5_pending kullanabilirsin (limitler sistemce zorlanır). Lot için mt5_risksize hesapla ya da mt5_trade’e riskPct ver; SL/TP broker stops_level mesafesine uymalı.',
8717
9397
  roleDef
8718
9398
  ? 'Bulgularını agent_dm ile ANA TRADER\u2019a bildir (to: "Trader" ya da ajan başlığı anahtarı); teknik/öneri çelişkisi varsa gerekçenle yaz.'
8719
9399
  : f.strategy ? `Sahibinin strateji notu: ${f.strategy}` : 'Strateji notu yok: trend + destek/direnç + momentum ile temel okuma yap.',
8720
9400
  !roleDef && f.strategy ? `Sahibinin strateji notu: ${f.strategy}` : '',
8721
- 'Bu turda: mt5_status → hesap/pozisyon/fiyat verisi → değerlendirme → kararlar (veya BEKLE: sebep) → kısa rapor.',
9401
+ 'Bu turda: mt5_status → hesap/pozisyon/fiyat verisi → mt5_rates/mt5_indicators ile teknik okuma → değerlendirme → kararlar (veya BEKLE: sebep) → kısa rapor.',
9402
+ 'Önemli kararların gerekçesini mt5_note ile günlüğe yaz (haftalık performans raporu bu notları kullanır).',
9403
+ 'Risk otomasyonu main süreçte 5 sn döngüyle çalışır (+R BE, trailing, kısmi TP) — sen yine de SL/TP seviyelerini aktif yönet.',
8722
9404
  'Diğer finance/paralel ajanlarla koordinasyon için agent_dm aracı var (to: ajan başlığı anahtar kelimesi).',
8723
9405
  ].filter(Boolean).join('\n');
8724
9406
  }
@@ -8795,7 +9477,7 @@ function finAgentRound(sid) {
8795
9477
  ? `UZMANLIK: ${roleDef.desc} — İŞLEM AÇMA, yalnız analiz + net öneri.`
8796
9478
  : 'İşlem açabilirsin — limitlere uy, SL\u2019siz pozisyon bırakma.';
8797
9479
  const focus = agent.symbols.length ? `Odak: ${agent.symbols.join(', ')}. ` : '';
8798
- const round = `FINANCE TUR #${agent.round}: ${focus}hesap + pozisyonlar + fiyatları çek; ${roleDef ? 'rolüne uygun analiz yap ve öneri ver.' : 'açık pozisyonları yönet (SL/TP güncelle, hedefe ulaşanı kapat); stratejine göre yeni fırsatları değerlendir.'} ${auto} Kısa rapor ver.`;
9480
+ const round = `FINANCE TUR #${agent.round}: ${focus}hesap + pozisyonlar + fiyatları çek; ${roleDef ? 'rolüne uygun analiz yap (mt5_rates/mt5_indicators ile) ve öneri ver.' : 'açık pozisyonları yönet (SL/TP güncelle, hedefe ulaşanı kapat); mt5_rates/mt5_indicators ile yeni fırsatları değerlendir.'} ${auto} Önemli kararların gerekçesini mt5_note ile günlüğe yaz. Kısa rapor ver.`;
8799
9481
  const launch = (planBlock) => {
8800
9482
  if (!financeState.agents.has(String(sid))) return;
8801
9483
  const ok = engine.send(sid, planBlock + round, { userAction: false });
@@ -8888,6 +9570,11 @@ ipcMain.handle('finance:snapshot', async () => {
8888
9570
  roles: FIN_ROLES,
8889
9571
  rolesAuto: FIN_ROLES_AUTO,
8890
9572
  trader: { on: financeState.traderOn, busy, rounds: financeState.traderRounds, lastAt: financeState.lastRoundAt },
9573
+ /* performans + risk otomasyonu görünümü */
9574
+ stats: financeState.stats || null,
9575
+ equity: (financeState.equity || []).slice(-180),
9576
+ alerts: (financeState.alerts || []).slice(0, 50),
9577
+ watch: { on: !!financeState.watchTimer, managed: financeState.watch.size, lastAt: financeState.watchTickAt || 0 },
8891
9578
  };
8892
9579
  });
8893
9580
 
@@ -8965,6 +9652,30 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
8965
9652
  }
8966
9653
  if (p.maxLot !== undefined) f.maxLot = Math.max(0.01, Math.min(100, Number(p.maxLot) || 0.1));
8967
9654
  if (p.maxPositions !== undefined) f.maxPositions = Math.max(1, Math.min(20, Math.round(Number(p.maxPositions) || 3)));
9655
+ /* RİSK OTOMASYONU + BİLDİRİM ayarları */
9656
+ if (p.watchdog !== undefined) f.watchdog = !!p.watchdog;
9657
+ if (p.beOnR !== undefined) f.beOnR = Math.max(0, Math.min(10, Number(p.beOnR) || 0));
9658
+ if (p.beOffsetR !== undefined) f.beOffsetR = Math.max(0, Math.min(1, Number(p.beOffsetR) || 0));
9659
+ if (p.trailStartR !== undefined) f.trailStartR = Math.max(0, Math.min(10, Number(p.trailStartR) || 0));
9660
+ if (p.trailR !== undefined) f.trailR = Math.max(0, Math.min(5, Number(p.trailR) || 0));
9661
+ if (p.partialR !== undefined) f.partialR = Math.max(0, Math.min(10, Number(p.partialR) || 0));
9662
+ if (p.partialPct !== undefined) f.partialPct = Math.max(0, Math.min(90, Number(p.partialPct) || 0));
9663
+ if (p.notifyTarget !== undefined) {
9664
+ const t = String(p.notifyTarget || 'auto').toLowerCase();
9665
+ f.notifyTarget = ['auto', 'whatsapp', 'telegram', 'discord', 'off'].includes(t) ? t : 'auto';
9666
+ }
9667
+ if (p.notifyTrades !== undefined) f.notifyTrades = !!p.notifyTrades;
9668
+ if (p.notifyWatchdog !== undefined) f.notifyWatchdog = !!p.notifyWatchdog;
9669
+ if (p.maxDailyLossPct !== undefined) f.maxDailyLossPct = Math.max(0, Math.min(50, Number(p.maxDailyLossPct) || 0));
9670
+ if (p.dailyLossAction !== undefined) {
9671
+ const v = String(p.dailyLossAction || 'warn').toLowerCase();
9672
+ f.dailyLossAction = ['warn', 'stop', 'flatten'].includes(v) ? v : 'warn';
9673
+ }
9674
+ if (p.riskPerTradePct !== undefined) f.riskPerTradePct = Math.max(0, Math.min(20, Number(p.riskPerTradePct) || 0));
9675
+ if (p.maxPerSymbol !== undefined) f.maxPerSymbol = Math.max(0, Math.min(20, Math.round(Number(p.maxPerSymbol) || 0)));
9676
+ if (p.maxSameSide !== undefined) f.maxSameSide = Math.max(0, Math.min(20, Math.round(Number(p.maxSameSide) || 0)));
9677
+ if (p.minMarginLevel !== undefined) f.minMarginLevel = Math.max(0, Math.min(1000, Number(p.minMarginLevel) || 0));
9678
+ if (p.weeklyReport !== undefined) f.weeklyReport = !!p.weeklyReport;
8968
9679
  /* allowTrading artık AYARLANMAZ — daima true (finCfg zorlar) */
8969
9680
  if (p.strategy !== undefined) f.strategy = String(p.strategy || '').slice(0, 2000);
8970
9681
  if (p.analysisTeam !== undefined) f.analysisTeam = finRolesValid(p.analysisTeam);
@@ -9151,6 +9862,17 @@ ipcMain.handle('finance:install', async () => {
9151
9862
  return { ok: true, installing: true };
9152
9863
  });
9153
9864
 
9865
+ /* Haftalık raporu elle üret (panel butonu) — istatistik + günlük + ajan yorumu */
9866
+ ipcMain.handle('finance:report', async () => {
9867
+ try {
9868
+ const r = await finWeeklyReport(true);
9869
+ if (!r) return { ok: false, error: 'rapor üretilemedi' };
9870
+ return { ok: true, path: r.path, stats: r.stats || null };
9871
+ } catch (e) {
9872
+ return { ok: false, error: String((e && e.message) || e) };
9873
+ }
9874
+ });
9875
+
9154
9876
 
9155
9877
  /* düşünme (reasoning) seviyesi */
9156
9878
  ipcMain.handle('think:set', (_e, v) => {
@@ -10902,6 +11624,10 @@ ipcMain.handle('email:set', (_e, cfg) => {
10902
11624
  saveSettings();
10903
11625
  return { ...settings.email, pass: settings.email.pass ? '***' : '' };
10904
11626
  });
11627
+ /* E-posta paneli: gelen kutusu + okuma + gönderim (engine köprüsüyle aynı fonksiyonlar) */
11628
+ ipcMain.handle('email:list', (_e, opts) => emailList(opts || {}));
11629
+ ipcMain.handle('email:read', (_e, uid) => emailRead(uid));
11630
+ ipcMain.handle('email:send', (_e, msg) => emailSend(msg || {}));
10905
11631
 
10906
11632
  /* ---------- ekran görüntüsü ---------- */
10907
11633