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/package.json +1 -1
- package/src/agent/engine.js +1 -1
- package/src/agent/financetools.js +316 -45
- package/src/agent/finindicators.js +254 -0
- package/src/agent/finrisk.js +132 -0
- package/src/agent/finstats.js +229 -0
- package/src/agent/finwatch.js +110 -0
- package/src/agent/scripts/mt5_bridge.py +65 -0
- package/src/main.js +735 -9
- package/src/preload.js +6 -0
- package/src/renderer/i18n.js +90 -0
- package/src/renderer/index.html +93 -0
- package/src/renderer/renderer.js +490 -12
- package/src/renderer/style.css +36 -0
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
|
-
/*
|
|
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 === '
|
|
8463
|
-
else if (k === '
|
|
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);
|
|
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
|
|