beast-agent 2.32.0 → 2.33.1

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
@@ -3714,6 +3714,7 @@ app.whenReady().then(() => {
3714
3714
 
3715
3715
  app.on('before-quit', () => {
3716
3716
  app.isQuitting = true;
3717
+ try { finWatchStop(); } catch {} // finance watchdog zamanlayıcısını kapat
3717
3718
  flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
3718
3719
  try { toolsMod.disposeShellSessions(); } catch {} // kalıcı shell oturumlarını kapat
3719
3720
  try { require('./agent/mcp').stopAll(); } catch {} // MCP server süreçlerini kapat
@@ -8417,6 +8418,8 @@ function studioWatchStop() {
8417
8418
  const mt5bridge = require('./mt5bridge');
8418
8419
  const financetools = require('./agent/financetools');
8419
8420
  const customtools = require('./agent/customtools');
8421
+ const finwatch = require('./agent/finwatch');
8422
+ const finstats = require('./agent/finstats');
8420
8423
 
8421
8424
  const financeState = {
8422
8425
  mode: false, /* chat oturumları finance bayrağı alıyor mu */
@@ -8427,6 +8430,20 @@ const financeState = {
8427
8430
  installing: false,
8428
8431
  installTried: false,
8429
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,
8430
8447
  };
8431
8448
 
8432
8449
  function financeDir() {
@@ -8468,6 +8485,30 @@ function finCfg() {
8468
8485
  if (!Number(f.intervalSec)) f.intervalSec = 120;
8469
8486
  if (!Number(f.maxLot)) f.maxLot = 0.1;
8470
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;
8471
8512
  return f;
8472
8513
  }
8473
8514
 
@@ -8503,21 +8544,605 @@ function finPush(fn, data) {
8503
8544
  } catch {}
8504
8545
  }
8505
8546
 
8506
- /* 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 */
8507
9109
  try {
8508
9110
  financetools.setConfig(() => finCfg());
9111
+ financetools.setAlerts(finAlertApi());
8509
9112
  financetools.setNotify((entry) => {
8510
9113
  const k = String((entry && entry.kind) || '');
8511
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
+ }
8512
9134
  let line = '';
8513
- if (k === 'trade') line = `İŞLEM AÇILDI: ${d.side} ${d.volume} ${d.symbol}`;
8514
- else if (k === 'close') line = `POZİSYON KAPANDI: ticket ${d.ticket}${d.volume ? ' (' + d.volume + ' lot)' : ''}`;
8515
- else if (k === 'modify') line = `SL/TP GÜNCELLENDİ: ticket ${d.ticket} (SL ${d.sl || 0} / TP ${d.tp || 0})`;
8516
- 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 : ''}`;
8517
9138
  else if (k === 'cancel') line = `EMİR İPTAL: ticket ${d.ticket}`;
8518
9139
  if (line) {
8519
9140
  financeLog('[ajan] ' + line);
8520
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(() => {});
8521
9146
  }
8522
9147
  });
8523
9148
  } catch {}
@@ -8560,6 +9185,7 @@ function financeEnsureBridge() {
8560
9185
  candidates,
8561
9186
  terminal: String(f.terminalPath || '').trim(),
8562
9187
  });
9188
+ finWatchStart(); /* risk otomasyonu + bildirimler köprü açılırken başlar */
8563
9189
  return mt5bridge.status();
8564
9190
  }
8565
9191
 
@@ -8597,6 +9223,8 @@ mt5bridge.on('bridge', (m) => {
8597
9223
  if (m && m.connected) {
8598
9224
  financeLog('[MT5] terminal bağlı: ' + ((m.terminal && (m.terminal.name + ' @ ' + m.terminal.company)) || 'MT5'));
8599
9225
  if (m.account) financeLog(`[MT5] hesap ${m.account.login} · bakiye ${m.account.balance} ${m.account.currency}`);
9226
+ finWatchStart();
9227
+ finStatsRefresh(true).catch(() => {});
8600
9228
  } else if (m && m.error) {
8601
9229
  financeLog('[MT5] bağlantı yok — ' + m.error);
8602
9230
  if (/paket/i.test(m.error)) financeTryInstall();
@@ -8765,12 +9393,14 @@ function finTraderBrief(agent) {
8765
9393
  `Tur aralığı: ${f.intervalSec} sn · Max lot: ${f.maxLot} · Max eşzamanlı pozisyon: ${f.maxPositions}`,
8766
9394
  roleDef
8767
9395
  ? `UZMANLIK: ${roleDef.desc} — raporlarını bu çerçevede yaz; İŞLEM AÇMA, yalnız analiz + net öneri üret.`
8768
- : '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ı.',
8769
9397
  roleDef
8770
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.'
8771
9399
  : f.strategy ? `Sahibinin strateji notu: ${f.strategy}` : 'Strateji notu yok: trend + destek/direnç + momentum ile temel okuma yap.',
8772
9400
  !roleDef && f.strategy ? `Sahibinin strateji notu: ${f.strategy}` : '',
8773
- '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.',
8774
9404
  'Diğer finance/paralel ajanlarla koordinasyon için agent_dm aracı var (to: ajan başlığı anahtar kelimesi).',
8775
9405
  ].filter(Boolean).join('\n');
8776
9406
  }
@@ -8847,7 +9477,7 @@ function finAgentRound(sid) {
8847
9477
  ? `UZMANLIK: ${roleDef.desc} — İŞLEM AÇMA, yalnız analiz + net öneri.`
8848
9478
  : 'İşlem açabilirsin — limitlere uy, SL\u2019siz pozisyon bırakma.';
8849
9479
  const focus = agent.symbols.length ? `Odak: ${agent.symbols.join(', ')}. ` : '';
8850
- 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.`;
8851
9481
  const launch = (planBlock) => {
8852
9482
  if (!financeState.agents.has(String(sid))) return;
8853
9483
  const ok = engine.send(sid, planBlock + round, { userAction: false });
@@ -8940,6 +9570,11 @@ ipcMain.handle('finance:snapshot', async () => {
8940
9570
  roles: FIN_ROLES,
8941
9571
  rolesAuto: FIN_ROLES_AUTO,
8942
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 },
8943
9578
  };
8944
9579
  });
8945
9580
 
@@ -9017,6 +9652,30 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
9017
9652
  }
9018
9653
  if (p.maxLot !== undefined) f.maxLot = Math.max(0.01, Math.min(100, Number(p.maxLot) || 0.1));
9019
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;
9020
9679
  /* allowTrading artık AYARLANMAZ — daima true (finCfg zorlar) */
9021
9680
  if (p.strategy !== undefined) f.strategy = String(p.strategy || '').slice(0, 2000);
9022
9681
  if (p.analysisTeam !== undefined) f.analysisTeam = finRolesValid(p.analysisTeam);
@@ -9203,6 +9862,17 @@ ipcMain.handle('finance:install', async () => {
9203
9862
  return { ok: true, installing: true };
9204
9863
  });
9205
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
+
9206
9876
 
9207
9877
  /* düşünme (reasoning) seviyesi */
9208
9878
  ipcMain.handle('think:set', (_e, v) => {