beast-agent 2.55.0 → 2.57.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.55.0",
4
+ "version": "2.57.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -76,12 +76,28 @@ function call(requests) {
76
76
  try { args = JSON.parse(fs.readFileSync(0, 'utf8') || '{}'); } catch (e) {}
77
77
  const ticket = Number(args.ticket || 0);
78
78
  if (!ticket) return console.log(JSON.stringify({ ok: false, error: 'ticket zorunlu' }));
79
+ /* KISMİ KAPATMA (kısmi TP / kısmi stop): percent verilirse pozisyon
80
+ hacminden hesaplanır; hacim verilmezse tamamı kapanır. Kalan kısım
81
+ broker minimumunun (0.01) altındaysa tamamı kapatılır. */
82
+ const percent = Math.max(0, Math.min(100, Number(args.percent) || 0));
83
+ let volume = Number(args.volume || 0);
84
+ let pvol = 0;
85
+ if (percent > 0 && !(volume > 0)) {
86
+ const pr = await call([{ method: 'positions', params: {} }]);
87
+ const list = (pr.ok && pr.data && pr.data.positions) || [];
88
+ const pos = list.find((p) => Number(p.ticket) === ticket);
89
+ if (!pos) return console.log(JSON.stringify({ ok: false, error: 'pozisyon bulunamadi: ' + ticket }));
90
+ pvol = Number(pos.volume) || 0;
91
+ volume = Math.floor(((pvol * percent) / 100) * 100) / 100;
92
+ if (!(volume > 0)) return console.log(JSON.stringify({ ok: false, error: 'percent sonucu hacim sifir cikti (pozisyon ' + pvol + ' lot)' }));
93
+ if (pvol - volume > 0 && pvol - volume < 0.01 - 1e-9) volume = pvol; /* kalan minimumun altinda → tamami */
94
+ }
79
95
  const params = { ticket: ticket };
80
- if (args.volume) params.volume = Number(args.volume);
96
+ if (volume > 0) params.volume = volume;
81
97
  const r = await call([{ method: 'close', params: params }]);
82
98
  if (r.ok && r.data && r.data.result) {
83
99
  const res = r.data.result;
84
- console.log(JSON.stringify({ ok: true, retcode: res.retcode, comment: res.comment, detay: r.data }));
100
+ console.log(JSON.stringify({ ok: true, retcode: res.retcode, comment: res.comment, kismi: pvol > 0 && volume < pvol, kapatilan: volume || 'tamami', detay: r.data }));
85
101
  } else {
86
102
  console.log(JSON.stringify({ ok: false, error: r.error || 'kapatma reddedildi' }));
87
103
  }
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "mt5_kapat",
3
- "description": "Açık pozisyonu KAPATIR: ticket (pozisyon no) zorunlu. volume verilirse kısmi kapatır. Risk yönetimi için kullanılır. Başarıda kapanış sonucu döner.",
3
+ "description": "Açık pozisyonu kapatır — TAM ya da KISMİ: ticket zorunlu; percent (pozisyonun % kaçı, ör. 50) ya da volume (lot) verilebilir. Kısmi kapatma = kısmi TP (kâr al) / kısmi stop (zarar kes) — kararı ajan verir, otomatik değildir. Kalan kısım SL/TP ile devam eder; kalan lot 0.01 altında kalırsa tamamı kapanır. Başarıda kapanış sonucu döner.",
4
4
  "parameters": {
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "ticket": { "type": "number", "description": "Kapatilacak pozisyonun ticket numarasi" },
8
- "volume": { "type": "number", "description": "Kapatilacak lot (bos = tamami)" }
8
+ "percent": { "type": "number", "description": "Kapatilacak yuzde (ör. 50) — kısmi TP/kısmi stop icin" },
9
+ "volume": { "type": "number", "description": "Kapatilacak lot (percent yerine; ikisi de yoksa tamami kapanir)" }
9
10
  },
10
11
  "required": ["ticket"]
11
12
  },
12
- "updatedAt": "2026-09-09T18:48:00.000Z"
13
+ "defaultsVersion": 2,
14
+ "updatedAt": "2026-09-15T15:00:00.000Z"
13
15
  }
@@ -1957,20 +1957,20 @@ class Engine {
1957
1957
  const rules = memory.listRules();
1958
1958
  const isWorker = !!(session && session.bgJob && !isTrader);
1959
1959
  const modeBlock2 = isWorker
1960
- ? 'ROL: FİNANS İŞÇİSİ 🔎 — TEK bir görev için piyasa verisi toplar, analiz eder, RAPOR döndürürsün. mt5_trade/mt5_pending/mt5_close KULLANMA — yalnız okuma araçları (mt5_status/account/market/positions/orders/history). İş bitince kısa final raporu yaz.'
1960
+ ? 'ROL: FİNANS İŞÇİSİ 🔎 — TEK bir görev için piyasa verisi toplar, analiz eder, RAPOR döndürürsün. mt5_trade/mt5_pending KULLANMA (yeni işlem AÇMA) — açık pozisyonlarda kısmi kapatma (mt5_close percent: kısmi TP/kısmi stop) ve SL/TP güncelleme (mt5_modify) SERBEST; okuma araçları (mt5_status/account/market/positions/orders/history) her zaman açık. İş bitince kısa final raporu yaz.'
1961
1961
  : modeBlock;
1962
1962
  /* ANALİZ EKİBİ rolü: trader'ın yanında koşan uzman ajanlar (risk/teknik/
1963
1963
  makro) — hepsi İşlem AÇMAZ, kendi uzmanlık çerçevesinde rapor yazar */
1964
1964
  const roleBlock =
1965
1965
  ({
1966
1966
  risk:
1967
- 'ROL: RİSK AJANI 🛡 — portföyün RİSK GÖZCÜSÜSÜN, İşlem AÇMAZSIN (mt5_trade/mt5_pending/mt5_close KULLANMA). Her turda: hesap + pozisyonlar + marj kullanımı → kaldıraç/exposure değerlendirmesi; SL\u2019siz pozisyon, limit aşımı, tek yönlü birikme, günlük kayıp hızı risklerini raporla; net düzeltme önerisi ver (hangi pozisyon küçültülmeli/kapatılmalı).',
1967
+ 'ROL: RİSK AJANI 🛡 — portföyün RİSK GÖZCÜSÜSÜN, İşlem AÇMAZSIN (mt5_trade/mt5_pending KULLANMA — yeni işlem AÇMAZSIN; açık pozisyonda kısmi kapatma (mt5_close percent) ve SL/TP güncelleme (mt5_modify) SERBEST). Her turda: hesap + pozisyonlar + marj kullanımı → kaldıraç/exposure değerlendirmesi; SL\u2019siz pozisyon, limit aşımı, tek yönlü birikme, günlük kayıp hızı risklerini raporla; net düzeltme önerisi ver (hangi pozisyon küçültülmeli/kapatılmalı).',
1968
1968
  technic:
1969
- 'ROL: TEKNİK ANALİZ AJANI 📊 — fiyat yapısı uzmanısın, İşlem AÇMAZSIN (mt5_trade/mt5_pending/mt5_close KULLANMA). Her turda odak semboller için: trend/yapı, destek-direnç bölgeleri, momentum (mt5_market + mt5_history verisiyle); sembol başına AL/SAT/BEKLE + giriş/SL/TP fikri üret — trader bu öneriyi işleme çevirir. Bulgularını mt5_ea action:"note" ile grafiğe ÇİZ (destek/direnç/SL/TP seviyeleri) ve tool__mt5_shot ile seviyelerin göründüğünü teyit et; önemli seviyeleri görselle birlikte agent_dm ile trader\'a/ekibe ilet — grafik üzerinde ortak karar alın.',
1969
+ 'ROL: TEKNİK ANALİZ AJANI 📊 — fiyat yapısı uzmanısın, İşlem AÇMAZSIN (mt5_trade/mt5_pending KULLANMA — yeni işlem AÇMAZSIN; açık pozisyonda kısmi kapatma (mt5_close percent) ve SL/TP güncelleme (mt5_modify) SERBEST). Her turda odak semboller için: trend/yapı, destek-direnç bölgeleri, momentum (mt5_market + mt5_history verisiyle); sembol başına AL/SAT/BEKLE + giriş/SL/TP fikri üret — trader bu öneriyi işleme çevirir. Bulgularını mt5_ea action:"note" ile grafiğe ÇİZ (destek/direnç/SL/TP seviyeleri) ve tool__mt5_shot ile seviyelerin göründüğünü teyit et; önemli seviyeleri görselle birlikte agent_dm ile trader\'a/ekibe ilet — grafik üzerinde ortak karar alın.',
1970
1970
  macro:
1971
- 'ROL: MAKRO AJANI 🌍 — büyük resim uzmanısın, İşlem AÇMAZSIN (mt5_trade/mt5_pending/mt5_close KULLANMA). Her turda web_search ile güncel makro manşetleri + ekonomik takvim riskleri (faiz, CPI, jeopolitik); DXY/altın/petrol bağıntılarını odak sembollere çevir; sembol başına yön eğilimi + TEMKİN/BEKLE notu ver.',
1971
+ 'ROL: MAKRO AJANI 🌍 — büyük resim uzmanısın, İşlem AÇMAZSIN (mt5_trade/mt5_pending KULLANMA — yeni işlem AÇMAZSIN; açık pozisyonda kısmi kapatma (mt5_close percent) ve SL/TP güncelleme (mt5_modify) SERBEST). Her turda web_search ile güncel makro manşetleri + ekonomik takvim riskleri (faiz, CPI, jeopolitik); DXY/altın/petrol bağıntılarını odak sembollere çevir; sembol başına yön eğilimi + TEMKİN/BEKLE notu ver.',
1972
1972
  visual:
1973
- 'ROL: GÖRSEL ANALİZ AJANI 👁 — grafik/ekran görüntüsü uzmanısın, İşlem AÇMAZSIN (mt5_trade/mt5_pending/mt5_close KULLANMA). MT5 grafiği için ÖNCE tool__mt5_shot kullan (BeastFinance EA "shot": gerçek grafik PNG + görsel SONRAKİ TURDA gözüne gelir; symbol/timeframe vererek başka sembol/periyot çekebilirsin; dönen path send_file ile gönderilebilir); gerekirse EA panosuna mt5_ea action:"note" ile plan/seviye yaz. ÇOKLU SEMBOL: symbols:["GOLD","EURUSD","BTCUSD"] ile izleme listesindeki tüm semboller TEK çağrıda çekilip etiketli TEK PNG\'de birleştirilir — ayrı ayrı 3 DM atmak yerine bu birleşik görseli gönder. web grafiklerinde browser_open + browser_screenshot; bilgisayar ekranı için computer_look; görsel göremeyen metin-model isen ocr_read (source:"screen"/"browser") kullan. Gördüğün yapıyı yorumla: trend, formasyon, mum yapısı, destek-direnç ve SL/TP çizgileri; sembol başına GÖRSEL TEYİT + AL/SAT/BEKLE notu üret. Önemli bulguyu GÖRSELİYLE paylaş: agent_dm image:true (son ekran görüntün) ya da image:"<mt5_shot path>" ile ANA TRADER\'a/ekibe gönder — karşı ajan görüntüyü GERÇEKTEN görür, AJAN DM panelinde açılır. NOT: image:true DAİMA oturumdaki EN SON görseli yollar — her sembolün fotoğrafını ayrı göndermen gerekirse dönen path\'leri kullan (image:"<path>").',
1973
+ 'ROL: GÖRSEL ANALİZ AJANI 👁 — grafik/ekran görüntüsü uzmanısın, İşlem AÇMAZSIN (mt5_trade/mt5_pending KULLANMA — yeni işlem AÇMAZSIN; açık pozisyonda kısmi kapatma (mt5_close percent) ve SL/TP güncelleme (mt5_modify) SERBEST). MT5 grafiği için ÖNCE tool__mt5_shot kullan (BeastFinance EA "shot": gerçek grafik PNG + görsel SONRAKİ TURDA gözüne gelir; symbol/timeframe vererek başka sembol/periyot çekebilirsin; dönen path send_file ile gönderilebilir); gerekirse EA panosuna mt5_ea action:"note" ile plan/seviye yaz. ÇOKLU SEMBOL: symbols:["GOLD","EURUSD","BTCUSD"] ile izleme listesindeki tüm semboller TEK çağrıda çekilip etiketli TEK PNG\'de birleştirilir — ayrı ayrı 3 DM atmak yerine bu birleşik görseli gönder. web grafiklerinde browser_open + browser_screenshot; bilgisayar ekranı için computer_look; görsel göremeyen metin-model isen ocr_read (source:"screen"/"browser") kullan. Gördüğün yapıyı yorumla: trend, formasyon, mum yapısı, destek-direnç ve SL/TP çizgileri; sembol başına GÖRSEL TEYİT + AL/SAT/BEKLE notu üret. Önemli bulguyu GÖRSELİYLE paylaş: agent_dm image:true (son ekran görüntün) ya da image:"<mt5_shot path>" ile ANA TRADER\'a/ekibe gönder — karşı ajan görüntüyü GERÇEKTEN görür, AJAN DM panelinde açılır. NOT: image:true DAİMA oturumdaki EN SON görseli yollar — her sembolün fotoğrafını ayrı göndermen gerekirse dönen path\'leri kullan (image:"<path>").',
1974
1974
  })[String((session && session.financeRole) || '')] || '';
1975
1975
  /* ROL → SKILL eşleştirmesi (ayarlar modalı): rol başına TEK ve ZORUNLU
1976
1976
  skill; ANA TRADER için ayrıca PLAYBOOK skill'i (roleSkills.trader).
@@ -2009,9 +2009,10 @@ class Engine {
2009
2009
  modeBlock2 + '\n' +
2010
2010
  (roleBlockFull ? roleBlockFull + '\n' : '') +
2011
2011
  (teamLine || '') +
2012
- 'MT5 ARAÇLARI: mt5_status (bağlantı), mt5_account (hesap), mt5_market (canlı fiyat), mt5_positions (açık pozisyonlar), mt5_orders (bekleyen emirler), mt5_history (kapanan işlemler), mt5_alerts (fiyat alarmı: modu SEN seçersin — once/repeat), mt5_ea (BeastFinance grafik panosu: status/ping/chart/note), mt5_trade (TÜM TİPLER: buy/sell market + buy/sell limit/stop), mt5_close (kapat), mt5_modify (SL/TP), mt5_pending (TÜM TİPLER: buy_limit/sell_limit/buy_stop/sell_stop bekleyen + buy_market/sell_market anlık), mt5_cancel (emir iptal).\n' +
2012
+ 'MT5 ARAÇLARI: mt5_status (bağlantı), mt5_account (hesap), mt5_market (canlı fiyat), mt5_positions (açık pozisyonlar), mt5_orders (bekleyen emirler), mt5_history (kapanan işlemler), mt5_alerts (fiyat alarmı: modu SEN seçersin — once/repeat), mt5_ea (BeastFinance grafik panosu: status/ping/chart/note), mt5_trade (TÜM TİPLER: buy/sell market + buy/sell limit/stop), mt5_close (TAM ya da KISMİ kapat: percent ile kısmi TP & kısmi stop — karar ajanın), mt5_modify (SL/TP), mt5_pending (TÜM TİPLER: buy_limit/sell_limit/buy_stop/sell_stop bekleyen + buy_market/sell_market anlık), mt5_cancel (emir iptal).\n' +
2013
2013
  'EMİR TİPLERİ (6 tip — HEPSİ AÇIK): buy_market/sell_market = anlık piyasa emri (fiyat beklenmez), buy_limit/sell_limit = bekleyen limit (price zorunlu), buy_stop/sell_stop = bekleyen stop (price zorunlu). mt5_trade ve mt5_pending İKİSİ DE 6 tipi kabul eder; emir tipini net söyle (ör. type:"sell_limit").\n' +
2014
2014
  'ALARM KARARI SENDE: mt5_alerts ile alarm kurarken mode ZORUNLU — "once" (tek seferlik; ilk tetiklemede kapanır) ya da "repeat" (tekrarlı; alarm açık kalır, koşul sürdükçe cooldownMin dakikada bir tekrar uyarır + seni uyandırır; cooldownMin varsayılan 5). Tekrarlı mı tek seferlik mi olacağını sen seç, varsayılana bırakma; seviyenin önemine göre cooldownMin belirle (ör. 5/15/60).\n' +
2015
+ 'KISMİ KAPATMA YETKİSİ (TÜM finance ajanlarında): açık pozisyonda kısmi kapatma — kısmi TP (kâr al) ve kısmi stop (zarar kes) — TAMAMEN SENİN KARARIN, OTOMATİK DEĞİLDİR; mt5_close {ticket, percent, kind:"partial_tp"|"partial_sl", reason}. Gerek gördüğünde kullan; kalan pozisyon SL/TP ile devam eder. Kısmi TP fırsatı bildirimi gelirse (fiyat seviyeye ulaştı) kapatıp kapatmamak sana bağlı.\n' +
2015
2016
  'KİŞİSEL MT5 ARAÇLARI (VARSAYILAN KURULU — tool__* tüm oturumlarda çağrılabilir): tool__mt5_shot (grafikten GERÇEK PNG + görsel ajana enjekte — grafik işlerinde BİRİNCİL; timeframes:["M1","M15"] ile çoklu periyot, symbols:["GOLD","EURUSD","BTCUSD"] ile çoklu sembol TEK karede), tool__mt5_barlar2 (OHLC mumlar, köprüsüz), tool__mt5_m15_m5 (GOLD M15+M5 hazır paket), tool__mt5_fiyat, tool__mt5_durum (hesap+pozisyon+emir tek çağrı), tool__mt5_gecmis, tool__mt5_pozisyon_gecmis, tool__mt5_sltp, tool__mt5_kapat, tool__mt5_bekleyen, tool__mt5_emir, tool__mt5_emir_iptal.\n' +
2016
2017
  'BEASTFINANCE EA (OTOMATİK GRAFİK UZMANI — ENTEGRASYON KANALI): MT5 terminaline bağlanıldığında BeastFinance uzman danışmanı İLK GRAFİĞE OTOMATİK yüklenir, AutoTrading izni açılır (kurulum sistem tarafından yapılır; elle ekleme gerekmez). Grafik panosu + seviye çizgileri + dosya köprüsü (beast_ea.json / beast_cmd.json / beast_note.json) bu EA üzerinden yürür: entegrasyon işlerinde (status/ping/chart/note) mt5_ea kullan; ekran görüntüsü işlerinde tool__mt5_shot ("shot" komutu — gerçek PNG + ajan görseli) birincildir. Yazdığın not ve çizgiler tool__mt5_shot ve computer_look screenshot\'ında GÖRÜNÜR.\n' +
2017
2018
  'YETKİLERİN (AÇIK — çekinmeden kullan):\n' +
@@ -7108,6 +7109,11 @@ Engine.prototype._pushAgentDm = function (dm) {
7108
7109
  this._agentDms = this._agentDms || [];
7109
7110
  this._agentDms.push(dm);
7110
7111
  if (this._agentDms.length > 600) this._agentDms.splice(0, this._agentDms.length - 600);
7112
+ /* DIŞ KÖPRÜ KANCASI (main.js: Telegram): panel kaydı düşen her DM tek
7113
+ noktadan dışarı bildirilir — köprü hangi mesajı göndereceğine kendi karar verir. */
7114
+ if (typeof this.onAgentDm === 'function') {
7115
+ try { this.onAgentDm(dm); } catch {}
7116
+ }
7111
7117
  };
7112
7118
 
7113
7119
  /* ---------- EKİP GRUBU: aynı görevde koşan paralel ajanlar otomatik
@@ -7213,6 +7219,66 @@ Engine.prototype._agentTeamPost = function (gid, fromSid, fromTitle, text) {
7213
7219
  } catch {}
7214
7220
  };
7215
7221
 
7222
+ /* Grubu hazırla (yoksa kur): Telegram köprüsü ve finans alarmları grubu önceden
7223
+ hiç kurulmamış olsa da panele düşebilsin. */
7224
+ Engine.prototype._agentGroupEnsure = function (gid, title, opts) {
7225
+ gid = String(gid || '');
7226
+ if (!gid) return null;
7227
+ let g = this._agentGroups.get(gid);
7228
+ if (!g) {
7229
+ g = {
7230
+ id: gid,
7231
+ title: String(title || gid),
7232
+ members: [],
7233
+ titles: {},
7234
+ createdAt: nowIso(),
7235
+ closed: false,
7236
+ team: !(opts && opts.system),
7237
+ };
7238
+ this._agentGroups.set(gid, g);
7239
+ }
7240
+ if (title && !g.title) g.title = String(title);
7241
+ g.closed = false;
7242
+ delete g.closedAt;
7243
+ return g;
7244
+ };
7245
+
7246
+ /* DIŞ KAYNAKLI (kullanıcı/Telegram) GRUP POSTU: panelde gerçek mesaj olarak
7247
+ görünür + istenirse koşan üyelere DM gibi teslim edilir. Sistem postlarından
7248
+ farkı: model tarafına da düşebilir (wake kararı çağıranın). */
7249
+ Engine.prototype.agentDmGroupPost = function (input) {
7250
+ try {
7251
+ const i = input || {};
7252
+ const gid = String(i.gid || '');
7253
+ const g = this._agentGroupEnsure(gid, i.title || '', { system: !!i.system });
7254
+ if (!g) return { ok: false, error: 'gid gerekli' };
7255
+ const dm = {
7256
+ at: nowIso(),
7257
+ from: String(i.fromSid || 'user'),
7258
+ fromTitle: String(i.fromTitle || 'Kullanıcı'),
7259
+ to: '',
7260
+ toTitle: 'ekip',
7261
+ group: gid,
7262
+ groupTitle: g.title,
7263
+ topic: String(i.topic || 'grup').slice(0, 60),
7264
+ text: String(i.text || '').slice(0, 4000),
7265
+ ...(i.system ? { system: true } : {}),
7266
+ ...(i.viaTelegram ? { viaTelegram: true } : {}),
7267
+ };
7268
+ this._pushAgentDm(dm);
7269
+ /* canlı event TEK SEFER: ilk koşan üye (yoksa gönderen) — panelde çoğalmaz */
7270
+ const firstRunning = (g.members || []).find((m) => {
7271
+ const j = this._bgJobs && this._bgJobs.get(m);
7272
+ return j && j.status === 'running';
7273
+ });
7274
+ emitSafe(this, firstRunning || String(i.fromSid || ''), { type: 'agent-dm', ...dm });
7275
+ this._persistAgentDms();
7276
+ return { ok: true, group: g.title };
7277
+ } catch (e) {
7278
+ return { ok: false, error: String((e && e.message) || e) };
7279
+ }
7280
+ };
7281
+
7216
7282
  /* Sistem promptuna ekip zorunluluğu */
7217
7283
  Engine.prototype._agentTeamPromptLine = function (gid) {
7218
7284
  const g = this._agentGroups && this._agentGroups.get(String(gid));
@@ -282,12 +282,16 @@ const definitions = NAMES.map((name) => {
282
282
  },
283
283
  },
284
284
  mt5_close: {
285
- description: 'Açık pozisyonu kapatır: mt5_close {ticket, volume?}. volume verilirse kısmi kapatır. Risk yönetimi için kullanılır.',
285
+ description:
286
+ 'Pozisyonu kapatır — TAM ya da KISMİ: mt5_close {ticket, percent?|volume?, kind?:"partial_tp"|"partial_sl"|"close", reason?}. KISMİ KAPATMA (kâr al = kısmi TP, zarar kes = kısmi stop) tamamen SENİN KARARIN — otomatik yapılmaz; gerek gördüğünde kullan. percent: pozisyonun % kaçı kapatılsın (ör. 50), volume: lot; kalan pozisyon SL/TP ile devam eder. Kalan lot broker minimumunun altında kalırsa tamamı kapanır. Risk yönetiminin temel aracıdır.',
286
287
  parameters: {
287
288
  type: 'object',
288
289
  properties: {
289
290
  ticket: { type: 'number', description: 'Pozisyon ticket no' },
290
- volume: { type: 'number', description: 'Kapatılacak lot (boşsa tamamı)' },
291
+ percent: { type: 'number', description: 'Kapatılacak yüzde (ör. 50) — kısmi TP/kısmi stop için' },
292
+ volume: { type: 'number', description: 'Kapatılacak lot (percent yerine; boşsa ve percent yoksa tamamı kapanır)' },
293
+ kind: { type: 'string', enum: ['partial_tp', 'partial_sl', 'close'], description: 'Kısmi kapatmanın nedeni: kâr al / zarar kes (günlüğe etiket olur)' },
294
+ reason: { type: 'string', description: 'Kararın tezi (tek cümle, günlüğe yazılır)' },
291
295
  },
292
296
  required: ['ticket'],
293
297
  },
@@ -649,18 +653,48 @@ const handlers = {
649
653
  return { ok: true, opened: { symbol, side, volume: vol, sl, tp }, risk: riskInfo, result: data && data.result };
650
654
  },
651
655
  async mt5_close(args, ctx) {
652
- const cfg = getCfg();
653
656
  if (!mt5.running) return notConnected();
654
657
  const ticket = Number(args.ticket);
655
658
  if (!ticket) return { ok: false, error: 'ticket gerekli' };
659
+ /* KISMİ KAPATMA (kısmi TP / kısmi stop): kararı AJAN verir — otomatik
660
+ değildir. percent: pozisyonun % kaçı; volume: lot. Kalan pozisyon
661
+ SL/TP ile devam eder. */
662
+ const percent = Math.max(0, Math.min(100, Number(args.percent) || 0));
656
663
  let vol = Number(args.volume) || 0;
657
- if (vol > 0) {
658
- const maxLot = Number(cfg.maxLot) || 0.1;
659
- if (vol > maxLot) return { ok: false, error: `kısmi kapatma lotu limiti aşıyor (max ${maxLot})` };
664
+ let pos = null;
665
+ try {
666
+ const posRes = await bcall('positions', {}, 8000);
667
+ pos = ((posRes && posRes.positions) || []).find((x) => Number(x.ticket) === ticket) || null;
668
+ } catch {}
669
+ if (!pos) return { ok: false, error: 'pozisyon bulunamadı: ' + ticket };
670
+ const pvol = Number(pos.volume) || 0;
671
+ if (!(pvol > 0)) return { ok: false, error: 'pozisyon hacmi okunamadı: ' + ticket };
672
+ let info = null;
673
+ try { info = await symInfoRow(String(pos.symbol || '')); } catch {}
674
+ if (percent > 0 && !(vol > 0)) {
675
+ const norm = finrisk.normalizeVolume(info || {}, (pvol * percent) / 100);
676
+ if (norm.error) return { ok: false, error: norm.error };
677
+ vol = norm.volume;
660
678
  }
661
- const data = await bcall('close', { ticket, volume: vol || 0 }, 20000);
662
- noteTrade('close', { ticket, volume: vol || 'all', result: data && data.result }, ctx);
663
- return { ok: true, result: data && data.result };
679
+ if (!(vol > 0)) vol = pvol; /* hacim yok → tamamı */
680
+ if (vol > pvol + 1e-9) return { ok: false, error: `kapatılacak lot pozisyondan büyük (${vol} > ${pvol})` };
681
+ /* kalan pozisyon broker minimumunun altında kalacaksa TAMAMINI kapat */
682
+ const vmin = Number(info && info.volume_min) || 0.01;
683
+ const remain = pvol - vol;
684
+ if (remain > 0 && remain < vmin - 1e-9) vol = pvol;
685
+ const isPartial = vol < pvol - 1e-9;
686
+ const kind = String(args.kind || args.closeKind || '').toLowerCase();
687
+ const data = await bcall('close', { ticket, volume: vol }, 20000);
688
+ noteTrade('close', {
689
+ ticket,
690
+ volume: vol,
691
+ percent: percent || 0,
692
+ partial: isPartial,
693
+ closeKind: /partial_sl|kismi_stop|partial_stop/.test(kind) ? 'partial_sl' : /partial_tp|kismi_tp|partial_take/.test(kind) ? 'partial_tp' : kind,
694
+ reason: String(args.reason || '').slice(0, 300),
695
+ result: data && data.result,
696
+ }, ctx);
697
+ return { ok: true, closed: { ticket, volume: vol, partial: isPartial }, remaining: Math.round(remain * 1e8) / 1e8, result: data && data.result };
664
698
  },
665
699
  async mt5_modify(args, ctx) {
666
700
  if (!mt5.running) return notConnected();
@@ -106,6 +106,8 @@ class TelegramBridge {
106
106
  username: String(msg.from.username || ''),
107
107
  senderName: String(msg.from.first_name || msg.from.username || ''),
108
108
  isGroup: !!(msg.chat && (msg.chat.type === 'group' || msg.chat.type === 'supergroup')),
109
+ chatTitle: String((msg.chat && (msg.chat.title || msg.chat.username)) || ''),
110
+ chatType: String((msg.chat && msg.chat.type) || 'private'),
109
111
  };
110
112
  try {
111
113
  if (this.onIncoming) this.onIncoming(String(chatId), payload);
package/src/main.js CHANGED
@@ -332,6 +332,18 @@ let tgCronSid = null; // Telegram CRON TABAN OTURUMU: otomatik işler; hiçbir k
332
332
  let tgHistory = new Map(); // chatId -> [sid,...]
333
333
  const TG_HISTORY_CAP = 20;
334
334
  let tgLastActiveChatId = ''; // en son mesaj gelen TG sohbeti — cron yansıtma hedefi
335
+ /* Botun gördüğü Telegram GRUPLARI: chatId -> {title, at}. Ajan DM ↔ Telegram
336
+ köprüsü kurulumunda grup seçimi bu listeden yapılır (BotFather'da gizlilik
337
+ kapatılmalı ya da bot gruba admin olmalı ki mesajlar düşsün). */
338
+ let tgGroups = {};
339
+ try {
340
+ const raw = settings.tgGroups;
341
+ if (raw && typeof raw === 'object') {
342
+ for (const [id, v] of Object.entries(raw)) {
343
+ if (v && typeof v === 'object') tgGroups[String(id)] = { title: String(v.title || ''), at: Number(v.at) || 0 };
344
+ }
345
+ }
346
+ } catch {}
335
347
  let dc = null;
336
348
  let dcChats = new Map(); // discord channelId -> KENDİ oturumu: her kanal AYRI (gizlilik)
337
349
  let dcCronSid = null; // Discord CRON TABAN OTURUMU: otomatik işler; hiçbir kişiye bağlı DEĞİL
@@ -672,6 +684,29 @@ function tgLog(line) {
672
684
  try { log.info('telegram', line); } catch {}
673
685
  }
674
686
 
687
+ /* Gelen grup mesajından grubu hatırla — ajan DM köprüsü grup seçicisi bu
688
+ listeden beslenir. Başlık değişmedikçe diske yazılmaz (yalnız tazeleme). */
689
+ function tgRememberGroup(chatId, title) {
690
+ const id = String(chatId || '');
691
+ if (!id) return;
692
+ const cur = tgGroups[id];
693
+ const t = String(title || '');
694
+ const at = Date.now();
695
+ if (cur && (cur.title === t || !t)) {
696
+ cur.at = at;
697
+ return;
698
+ }
699
+ tgGroups[id] = { title: t || (cur && cur.title) || id, at };
700
+ settings.tgGroups = tgGroups;
701
+ try { saveSettings(); } catch {}
702
+ }
703
+
704
+ function tgGroupsList() {
705
+ return Object.entries(tgGroups)
706
+ .map(([id, v]) => ({ id, title: String((v && v.title) || id), at: Number((v && v.at) || 0) }))
707
+ .sort((a, b) => b.at - a.at);
708
+ }
709
+
675
710
  function tgFind(senderId, username) {
676
711
  const list = settings.tgAllow || [];
677
712
  if (!list.length) return null; // boş liste = kimseye cevap yok
@@ -2895,8 +2930,20 @@ async function tgFlush(chatId) {
2895
2930
  async function handleTgIncoming(chatId, payload) {
2896
2931
  try {
2897
2932
  if (!engine) return;
2898
- /* v1: yalnız birebir sohbetler — grup davranışı WA'daki gibi ayrı toggle ile gelir */
2933
+ /* GRUP: normal grup desteği kapalı; ancak AJAN DM ↔ TELEGRAM köprüsünde
2934
+ seçili grupsa mesaj AJAN DM grubuna düşer ve finance ajanları uyanır. */
2899
2935
  if (payload.isGroup) {
2936
+ tgRememberGroup(chatId, payload.chatTitle);
2937
+ const dmTg = finDmTgCfg();
2938
+ if (dmTg.on && String(dmTg.chat) === String(chatId)) {
2939
+ const allowed = tgFind(payload.senderId, payload.username);
2940
+ tgLog(
2941
+ `grup(ajan dm) chat=${chatId} sender=${payload.senderId || '?'} user=${payload.username || '-'} allowed=${!!allowed}`
2942
+ );
2943
+ if (!allowed) return; /* izin listesi dışı yoksay */
2944
+ finTelegramToAgentDm(payload);
2945
+ return;
2946
+ }
2900
2947
  tgLog(`skip: grup mesajı chat=${chatId} (grup desteği kapalı)`);
2901
2948
  return;
2902
2949
  }
@@ -3566,6 +3613,10 @@ function reloadBackend() {
3566
3613
  düşünce sıradaki planlı turu BEKLEMEZ — ajan boştaysa tur hemen başlar
3567
3614
  (ajan-ajan DM'lerinde engine kancayı çağırmaz; ping-pong korunur). */
3568
3615
  engine.onDmQueued = (job) => { try { finWakeAgent(job && job.id); } catch {} };
3616
+ /* AJAN DM → TELEGRAM: AJAN DM grubuna düşen her kayıt (alarm/sistem postu
3617
+ dahil) tek kancadan geçer; köprü ayarı açıksa seçili Telegram grubuna
3618
+ yansıtılır (Telegram'dan gelen mesajlar viaTelegram ile geri gönderilmez). */
3619
+ engine.onAgentDm = (dm) => { try { finAgentDmToTelegram(dm); } catch {} };
3569
3620
  /* PANEL RUN köprüsü: ajanın panel_run aracı → ÇALIŞTIR panelindeki yönetilen
3570
3621
  süreç koşucusu (sandbox:run IPC ile aynı makine). */
3571
3622
  engine.sbRunHook = sbRunStartManaged;
@@ -7683,14 +7734,15 @@ function watcherFire(w, value) {
7683
7734
  ? 'izlenen değer değişti'
7684
7735
  : `kural sağlandı (son değer ${value}, koşul ${w.op} ${w.value ?? ''})`;
7685
7736
  toastNotify(`İzleyici: ${w.name}`, target, 'watchers');
7686
- /* FINANCE AJANI: izleyici alarmı AJAN DM'i olarak düşer ve boştaki ajanı
7687
- tur beklemeden uyandırır (main.js finWakeAgentDm). Diğer oturumlarda
7688
- eski davranış: mesaj doğrudan sohbete iner. */
7689
- if (financeState.agents.has(String(sid))) {
7690
- finWakeAgentDm(
7691
- sid,
7737
+ /* FINANCE AJANI: izleyici alarmı AJAN DM GRUBUNA bildirim olarak düşer ve
7738
+ koşan TÜM finance ajanlarını tur beklemeden uyandırır (finWakeAgents:
7739
+ grup postu + dm+wake). Diğer oturumlarda eski davranış: mesaj doğrudan
7740
+ sohbete iner. */
7741
+ if (financeState.agents.has(String(sid)) || (financeState.mode && financeState.agents.size)) {
7742
+ finWakeAgents(
7692
7743
  `[FİNANS OLAYI — İZLEYİCİ ALARMI: ${w.name}]\n- ${target}\n` +
7693
- `Şimdi yap: izleyiciyi kurma nedenini hatırla; durumu değerlendir, gerekiyorsa işlem/uyarı üret. Kısa rapor ver.`
7744
+ `Şimdi yap: izleyiciyi kurma nedenini hatırla; durumu değerlendir, gerekiyorsa işlem/uyarı üret. Kısa rapor ver.`,
7745
+ { kind: 'watcher', watcher: w.id }
7694
7746
  );
7695
7747
  return;
7696
7748
  }
@@ -9349,6 +9401,12 @@ function finCfg() {
9349
9401
  iptal) düşer; fiyat alarmı, koruma (BE/trailing/kısmi TP), günlük limit ve
9350
9402
  rapor bildirimleri panelde kalır. false = tüm olaylar kanallara gider. */
9351
9403
  if (typeof f.notifyTradeOnly !== 'boolean') f.notifyTradeOnly = true;
9404
+ /* AJAN DM ↔ TELEGRAM KÖPRÜSÜ: açıkken `team:finance` AJAN DM grubundaki
9405
+ mesajlar seçilen Telegram grubuna düşer; o gruba yazılan mesaj AJAN DM
9406
+ grubunda görünür ve koşan finance ajanları tur beklemeden uyanır. */
9407
+ if (typeof f.dmTelegram !== 'boolean') f.dmTelegram = false;
9408
+ if (typeof f.dmTelegramChat !== 'string') f.dmTelegramChat = '';
9409
+ if (typeof f.dmTelegramTitle !== 'string') f.dmTelegramTitle = '';
9352
9410
  if (!Number.isFinite(Number(f.maxDailyLossPct))) f.maxDailyLossPct = 3;
9353
9411
  /* GÜNLÜK LİMİT AKSİYONU: warn = yalnız uyarı; stop = tüm finance ajanlarını
9354
9412
  durdur; flatten = durdur + tüm pozisyonları kapat */
@@ -10001,6 +10059,76 @@ async function finStatsRefresh(force) {
10001
10059
  }
10002
10060
  }
10003
10061
 
10062
+ /* ---- AJAN DM ↔ TELEGRAM KÖPRÜSÜ ----
10063
+ Beast Finance görünüm ayarlarından açılır: `team:finance` AJAN DM grubundaki
10064
+ her mesaj (alarm/sistem postları dahil) seçilen Telegram grubuna yansır;
10065
+ o gruba yazılan mesaj AJAN DM grubunda görünür ve koşan finance ajanları
10066
+ tur beklemeden uyandırılır. Ayarlar: finCfg().dmTelegram / dmTelegramChat. */
10067
+ const FIN_TEAM_GID = 'team:finance';
10068
+ const FIN_TEAM_TITLE = 'Beast Finance EKİP';
10069
+
10070
+ /* Köprü ayarı (tek nokta): açık + grup seçili mi? */
10071
+ function finDmTgCfg() {
10072
+ let f = {};
10073
+ try { f = finCfg(); } catch {}
10074
+ return {
10075
+ on: !!(f && f.dmTelegram && f.dmTelegramChat),
10076
+ chat: String((f && f.dmTelegramChat) || ''),
10077
+ title: String((f && f.dmTelegramTitle) || ''),
10078
+ };
10079
+ }
10080
+
10081
+ /* Giden: AJAN DM grubu → Telegram (Telegram'dan gelenler geri gönderilmez). */
10082
+ function finAgentDmToTelegram(dm) {
10083
+ try {
10084
+ if (!dm || dm.viaTelegram) return;
10085
+ if (String(dm.group || '') !== FIN_TEAM_GID) return;
10086
+ const c = finDmTgCfg();
10087
+ if (!c.on || !tg || !tg.connected) return;
10088
+ const who = String(dm.fromTitle || (dm.system ? 'SİSTEM' : 'Ajan'));
10089
+ const body =
10090
+ `👥 *${String(dm.groupTitle || FIN_TEAM_TITLE)}* · ${who}\n` +
10091
+ String(dm.text || '').slice(0, 3800);
10092
+ Promise.resolve(tg.send(c.chat, body)).catch((e) =>
10093
+ tgLog('ajan dm → telegram hata: ' + String((e && e.message) || e))
10094
+ );
10095
+ } catch {}
10096
+ }
10097
+
10098
+ /* Gelen: Telegram grubu → AJAN DM grubu (+ tüm koşan finance ajanlarını uyandır). */
10099
+ function finTelegramToAgentDm(payload) {
10100
+ try {
10101
+ const body = String((payload && payload.text) || '').trim();
10102
+ if (!body || !engine) return { ok: false };
10103
+ const who = String((payload && (payload.senderName || payload.username)) || 'Telegram');
10104
+ const label = `Sahip · Telegram (${who})`;
10105
+ try {
10106
+ if (typeof engine.agentDmGroupPost === 'function') {
10107
+ engine.agentDmGroupPost({
10108
+ gid: FIN_TEAM_GID,
10109
+ title: FIN_TEAM_TITLE,
10110
+ fromSid: 'tg:' + String((payload && payload.senderId) || ''),
10111
+ fromTitle: label,
10112
+ topic: 'telegram',
10113
+ text: body,
10114
+ viaTelegram: true,
10115
+ });
10116
+ }
10117
+ } catch {}
10118
+ const wake =
10119
+ `[AJAN DM — TELEGRAM GRUBU · ${label}]\n${body}\n` +
10120
+ 'Şimdi yap: sahibin Telegram grubundan gelen bu mesajı değerlendir; gerekiyorsa ekipçe aksiyon al. Kısa rapor ver.';
10121
+ let sent = 0;
10122
+ for (const [sid] of financeState.agents) {
10123
+ if (finWakeAgentDm(sid, wake)) sent += 1;
10124
+ }
10125
+ financeLog(`[telegram] ajan dm grubuna mesaj düştü (${sent} ajan uyandı)`);
10126
+ return { ok: true, sent };
10127
+ } catch {
10128
+ return { ok: false };
10129
+ }
10130
+ }
10131
+
10004
10132
  /* ---- FİNANS OLAY → AJAN DM UYANDIRMA ----
10005
10133
  Stop/TP/bekleyen emir aktivasyonu/fiyat alarmı gibi olaylar koşan finance
10006
10134
  ajanlarına AJAN DM'i olarak düşer ve BOŞTAKİ ajanı TUR SANİYESİNİ
@@ -10031,20 +10159,32 @@ function finWakeAgentDm(sid, text) {
10031
10159
  }
10032
10160
  }
10033
10161
 
10034
- /* Tüm koşan finance ajanlarını olayla uyandır. Dönüş: uyandırılan ajan sayısı. */
10162
+ /* Tüm koşan finance ajanlarını olayla uyandır. Dönüş: uyandırılan ajan sayısı.
10163
+ Olay HER durumda ekip DM grubuna sistem postu olarak düşer (koşan ajan yoksa
10164
+ bile panelde görünür — grup yoksa kurulur). */
10035
10165
  function finWakeAgents(text, meta) {
10036
10166
  const body = String(text || '').trim();
10037
10167
  if (!body || !engine) return 0;
10038
- if (!financeState.agents.size) {
10039
- financeLog('[olay] koşan finance ajanı yok — uyarı yalnız günlük/bildirimde' + (meta && meta.kind ? ' (' + meta.kind + ')' : ''));
10040
- return 0;
10041
- }
10042
10168
  /* Panel görünürlüğü: olay ekip DM grubuna sistem postu olarak düşer */
10043
10169
  try {
10044
- if (typeof engine._agentTeamPost === 'function') {
10170
+ if (typeof engine.agentDmGroupPost === 'function') {
10171
+ engine.agentDmGroupPost({
10172
+ gid: FIN_TEAM_GID,
10173
+ title: FIN_TEAM_TITLE,
10174
+ fromSid: 'finance',
10175
+ fromTitle: 'Beast Finance',
10176
+ topic: 'ortak görev',
10177
+ text: body,
10178
+ system: true,
10179
+ });
10180
+ } else if (typeof engine._agentTeamPost === 'function') {
10045
10181
  engine._agentTeamPost('team:finance', 'finance', 'Beast Finance', body);
10046
10182
  }
10047
10183
  } catch {}
10184
+ if (!financeState.agents.size) {
10185
+ financeLog('[olay] koşan finance ajanı yok — uyarı yalnız günlük/bildirimde' + (meta && meta.kind ? ' (' + meta.kind + ')' : ''));
10186
+ return 0;
10187
+ }
10048
10188
  let sent = 0;
10049
10189
  for (const [sid] of financeState.agents) {
10050
10190
  if (finWakeAgentDm(sid, body)) sent += 1;
@@ -10314,15 +10454,24 @@ async function finWatchTick() {
10314
10454
  finJournal({ kind: 'watchdog', action: 'sl-move', ticket, symbol: p.symbol, sl: act.sl });
10315
10455
  }
10316
10456
  } else if (act.kind === 'partial') {
10317
- const r = await mt5bridge.call('close', { ticket: p.ticket, volume: act.volume }, 15000).catch(() => null);
10318
- if (r && r.ok) {
10319
- st.partial = true;
10320
- const line = `🎯 Kısmi TP: ${p.symbol} #${ticket} ${act.volume} lot kapatıldı`;
10321
- financeLog('[watchdog] ' + line);
10322
- if (cfg.notifyWatchdog !== false) financeNotify(line, 'watchdog');
10323
- finJournal({ kind: 'watchdog', action: 'partial-tp', ticket, symbol: p.symbol, volume: act.volume });
10324
- finStatsRefresh(true);
10325
- }
10457
+ /* OTOMATİK KAPATMA YOK — kısmi TP/stop kararını AJAN verir: seviye
10458
+ gelince ajanlar TUR BEKLEMEDEN uyandırılır; uygun görürse
10459
+ mt5_close {percent, kind:"partial_tp"} ile kısmi kâr alır. */
10460
+ st.partial = true;
10461
+ const rNow = decision.r > 0
10462
+ ? Math.round(((Number(p.type) === 0 ? Number(p.price_current) - Number(p.price_open) : Number(p.price_open) - Number(p.price_current)) / decision.r) * 10) / 10
10463
+ : 0;
10464
+ const pct = Number(cfg.partialPct) || 50;
10465
+ const line = `🎯 KISMİ TP FIRSATI: ${p.symbol} #${ticket} +${rNow}R (önerilen %${pct}) — kapatma kararı ajanın`;
10466
+ financeLog('[watchdog] ' + line);
10467
+ if (cfg.notifyWatchdog !== false) financeNotify(line, 'watchdog');
10468
+ finJournal({ kind: 'watchdog', action: 'partial-tp-opportunity', ticket, symbol: p.symbol, r: rNow, pct });
10469
+ finWakeAgents(
10470
+ `[FİNANS OLAYI — KISMİ TP FIRSATI, TUR BEKLEMEDEN İLETİLDİ]\n` +
10471
+ `- ${p.symbol}${Number(p.type) === 0 ? ' BUY' : ' SELL'} #${ticket} +${rNow}R seviyesinde (önerilen kısmi kapatma: %${pct})\n` +
10472
+ `Karar SENİN (otomatik kapatma yok): gerek görürsen mt5_close {ticket:${ticket}, percent:${pct}, kind:"partial_tp", reason:"..."} ile kısmi kâr al; kalan pozisyon SL/TP ile devam eder. Uygun değilse dokunma. Kısa rapor ver.`,
10473
+ { kind: 'partial-tp', ticket }
10474
+ );
10326
10475
  } else if (act.kind === 'warnSL') {
10327
10476
  st.warnSL = true;
10328
10477
  const line = `⚠️ SL yaklaşıyor: ${p.symbol} #${ticket} (SL ${p.sl}, fiyat ${p.price_current})`;
@@ -10530,6 +10679,19 @@ function finTradeEvent(kRaw, dataRaw, sidRaw) {
10530
10679
  if (k === 'trade') {
10531
10680
  finJournal({ kind: 'trade', sid, agent: who, symbol: d.symbol, side: d.side, volume: d.volume, sl: d.sl, tp: d.tp, reason: d.reason || d.comment || '' });
10532
10681
  } else if (k === 'close') {
10682
+ /* KISMİ KAPATMA (kısmi TP / kısmi stop): ajan kararı; günlük + bildirim.
10683
+ Kesin toplam K/Z yine watchdog tarafından yazılır (çift sayım yok). */
10684
+ const isPartial = d.partial === true || Number(d.percent) > 0;
10685
+ if (isPartial) {
10686
+ const ck = String(d.closeKind || '');
10687
+ const label = ck === 'partial_sl' ? 'KISMİ STOP (zarar kes)' : ck === 'partial_tp' ? 'KISMİ TP (kâr al)' : 'KISMİ KAPATMA';
10688
+ finJournal({ kind: 'partial-close', sid, agent: who, ticket: d.ticket, volume: d.volume, percent: Number(d.percent) || 0, closeKind: ck, reason: d.reason || '' });
10689
+ const line = `✂️ ${label}: ticket ${d.ticket}${Number(d.percent) > 0 ? ` · %${Number(d.percent)}` : ''}${d.volume ? ` · ${d.volume} lot` : ''}${who ? ' · ' + who : ''}`;
10690
+ financeLog('[ajan] ' + line);
10691
+ finPush('trade', { line });
10692
+ if (finCfg().notifyTrades !== false) financeNotify(line, 'close', false);
10693
+ return;
10694
+ }
10533
10695
  finJournal({ kind: 'close-req', sid, agent: who, ticket: d.ticket, volume: d.volume });
10534
10696
  return; /* kesin kapanış K/Z'si watchdog tarafından yazılır (çift bildirim yok) */
10535
10697
  } else if (k === 'modify') {
@@ -10582,7 +10744,16 @@ try {
10582
10744
  } else if (kind === 'modify') finTradeEvent('modify', { ticket: a.ticket, sl: a.sl, tp: a.tp }, sid);
10583
10745
  else if (kind === 'pending') finTradeEvent('pending', { symbol: a.symbol, type: a.type, volume: a.volume, reason: a.reason || '' }, sid);
10584
10746
  else if (kind === 'cancel') finTradeEvent('cancel', { ticket: a.ticket }, sid);
10585
- else if (kind === 'close') finTradeEvent('close', { ticket: a.ticket, volume: a.volume || 'all' }, sid);
10747
+ else if (kind === 'close') {
10748
+ finTradeEvent('close', {
10749
+ ticket: a.ticket,
10750
+ volume: a.volume || (a.percent ? 0 : 'all'),
10751
+ percent: Number(a.percent) || 0,
10752
+ partial: !!(Number(a.percent) > 0 || (a.volume && String(a.volume) !== 'all')),
10753
+ closeKind: String(a.kind || a.closeKind || ''),
10754
+ reason: String(a.reason || ''),
10755
+ }, sid);
10756
+ }
10586
10757
  });
10587
10758
  } catch {}
10588
10759
 
@@ -11038,7 +11209,7 @@ function finTraderBrief(agent) {
11038
11209
  `Tur aralığı: ${f.intervalSec} sn · Max lot: ${f.maxLot} · Max eşzamanlı pozisyon: ${f.maxPositions}`,
11039
11210
  roleDef
11040
11211
  ? `UZMANLIK: ${roleDef.desc} — raporlarını bu çerçevede yaz; İŞLEM AÇMA, yalnız analiz + net öneri üret.`
11041
- : 'Otomatik işlem AÇIK (daima): 6 emir tipinin HEPSİ açık — buy_market/sell_market (anlık piyasa), buy_limit/sell_limit ve buy_stop/sell_stop (bekleyen; price zorunlu). mt5_trade ya da mt5_pending İKİSİ de tüm tipleri kabul eder; ayrıca mt5_close/mt5_modify/mt5_cancel açık (limitler sistemce zorlanır). ALARM: mt5_alerts ile kurarken modu SEN seç — mode:"once" tek seferlik, mode:"repeat" + cooldownMin (dk) tekrarlı. Lot için mt5_risksize hesapla ya da mt5_trade’e riskPct ver; SL/TP broker stops_level mesafesine uymalı.',
11212
+ : 'Otomatik işlem AÇIK (daima): 6 emir tipinin HEPSİ açık — buy_market/sell_market (anlık piyasa), buy_limit/sell_limit ve buy_stop/sell_stop (bekleyen; price zorunlu). mt5_trade ya da mt5_pending İKİSİ de tüm tipleri kabul eder; ayrıca mt5_close/mt5_modify/mt5_cancel açık (limitler sistemce zorlanır). KISMİ KAPATMA yetkisi: kısmi TP (kâr al) ve kısmi stop (zarar kes) — mt5_close {percent, kind:"partial_tp"|"partial_sl"}; otomatik DEĞİL, gerek görürsen sen kullan. ALARM: mt5_alerts ile kurarken modu SEN seç — mode:"once" tek seferlik, mode:"repeat" + cooldownMin (dk) tekrarlı. Lot için mt5_risksize hesapla ya da mt5_trade’e riskPct ver; SL/TP broker stops_level mesafesine uymalı.',
11042
11213
  roleDef
11043
11214
  ? '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.'
11044
11215
  : f.strategy ? `Sahibinin strateji notu: ${f.strategy}` : 'Strateji notu yok: trend + destek/direnç + momentum ile temel okuma yap.',
@@ -11442,6 +11613,13 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
11442
11613
  if (p.notifyTrades !== undefined) f.notifyTrades = !!p.notifyTrades;
11443
11614
  if (p.notifyWatchdog !== undefined) f.notifyWatchdog = !!p.notifyWatchdog;
11444
11615
  if (p.notifyTradeOnly !== undefined) f.notifyTradeOnly = !!p.notifyTradeOnly;
11616
+ /* AJAN DM ↔ TELEGRAM köprüsü ayarları */
11617
+ if (p.dmTelegram !== undefined) f.dmTelegram = !!p.dmTelegram;
11618
+ if (p.dmTelegramChat !== undefined) {
11619
+ f.dmTelegramChat = String(p.dmTelegramChat || '').trim().slice(0, 64);
11620
+ if (!f.dmTelegramChat) f.dmTelegram = false;
11621
+ }
11622
+ if (p.dmTelegramTitle !== undefined) f.dmTelegramTitle = String(p.dmTelegramTitle || '').trim().slice(0, 80);
11445
11623
  if (p.maxDailyLossPct !== undefined) f.maxDailyLossPct = Math.max(0, Math.min(50, Number(p.maxDailyLossPct) || 0));
11446
11624
  if (p.dailyLossAction !== undefined) {
11447
11625
  const v = String(p.dailyLossAction || 'warn').toLowerCase();
@@ -13846,6 +14024,8 @@ ipcMain.handle('tg:allow:set', (_e, list) => {
13846
14024
  saveSettings();
13847
14025
  return settings.tgAllow;
13848
14026
  });
14027
+ /* Botun gördüğü gruplar (AJAN DM ↔ TELEGRAM grup seçicisi) */
14028
+ ipcMain.handle('tg:groups:list', () => tgGroupsList());
13849
14029
  ipcMain.handle('tg:sessions', () => [...tgChats.values()]);
13850
14030
 
13851
14031
  /* Sohbet geçmişi etiketi (Telegram): "Telegram <ad|chatId>" */
package/src/preload.js CHANGED
@@ -221,6 +221,7 @@ contextBridge.exposeInMainWorld('beast', {
221
221
  tgStop: () => ipcRenderer.invoke('tg:stop'),
222
222
  tgGetAllow: () => ipcRenderer.invoke('tg:allow:get'),
223
223
  tgSetAllow: (list) => ipcRenderer.invoke('tg:allow:set', list),
224
+ tgGroupsList: () => ipcRenderer.invoke('tg:groups:list'),
224
225
  tgListSessions: () => ipcRenderer.invoke('tg:sessions'),
225
226
  onTgEvent: (cb) => ipcRenderer.on('tg:event', (_e, ev) => cb(ev)),
226
227
  dcGetStatus: () => ipcRenderer.invoke('dc:status:get'),
@@ -225,8 +225,8 @@
225
225
  <div class="fin-field"><span>BE (R)</span><input id="finBeOnR" type="number" min="0" max="10" step="0.1" title="Kâr kaç R'ye ulaşınca SL başabaşa çekilsin (0 = kapalı)" /></div>
226
226
  <div class="fin-field"><span>Trailing başlangıç (R)</span><input id="finTrailStartR" type="number" min="0" max="10" step="0.1" title="Kaç R kârdan sonra trailing başlasın (0 = kapalı)" /></div>
227
227
  <div class="fin-field"><span>Trailing mesafe (R)</span><input id="finTrailR" type="number" min="0" max="5" step="0.1" title="SL fiyattan kaç R geriden gelsin" /></div>
228
- <div class="fin-field"><span>Kısmi TP (R)</span><input id="finPartialR" type="number" min="0" max="10" step="0.1" title="Kaç R kârda kısmi kapatma yapılsın (0 = kapalı)" /></div>
229
- <div class="fin-field"><span>Kısmi TP (%)</span><input id="finPartialPct" type="number" min="0" max="90" step="5" title="Kısmi kapatmada kapanacak lot yüzdesi" /></div>
228
+ <div class="fin-field"><span>Kısmi TP (R)</span><input id="finPartialR" type="number" min="0" max="10" step="0.1" title="Kaç R kârda ajana kısmi TP fırsatı bildirilsin (0 = kapalı) — kapatma kararını AJAN verir, otomatik kapatılmaz" /></div>
229
+ <div class="fin-field"><span>Kısmi TP (%)</span><input id="finPartialPct" type="number" min="0" max="90" step="5" title="Fırsat bildiriminde ajana önerilecek kısmi kapatma yüzdesi (kapatma kararı ajanın)" /></div>
230
230
  <div class="fin-field"><span>Günlük kayıp uyarı (%)</span><input id="finMaxDailyLoss" type="number" min="0" max="50" step="0.5" title="Gün içi düşüş bu yüzdeyi geçince aksiyon uygulanır (0 = kapalı)" /></div>
231
231
  </div>
232
232
  <div class="fin-grid2">
@@ -891,6 +891,17 @@
891
891
  <span class="fsr-main"><b>AKIŞ</b><i>Ajan ve MT5 olay günlüğü</i></span>
892
892
  </label>
893
893
  <div class="fin-set-note">RİSK OTOMASYONU gizliyken koruma döngüsü (watchdog) kapatılır; tekrar gösterdiğinde ilgili kutucuktan açabilirsin.</div>
894
+ <div class="fin-set-sep"></div>
895
+ <label class="fin-set-row" title="Açıkken Beast Finance AJAN DM grubundaki mesajlar (alarm/sistem postları dahil) seçtiğin Telegram grubuna düşer; o gruba yazdığın mesaj AJAN DM grubunda görünür ve koşan finance ajanları tur beklemeden uyanır.">
896
+ <input id="finDmTgOn" type="checkbox" />
897
+ <span class="fsr-main"><b>AJAN DM ↔ TELEGRAM</b><i>Ajan DM grubu Telegram'da canlı — iki yönlü köprü</i></span>
898
+ </label>
899
+ <div id="finDmTgWrap" class="fin-set-sub" hidden>
900
+ <span class="fin-set-lbl">TELEGRAM GRUBU</span>
901
+ <select id="finDmTgChat" title="Botu eklediğin Telegram grubunu seç — liste, bottan mesaj alınan grupları gösterir"></select>
902
+ <button id="finDmTgRefresh" class="fin-set-btn" title="Grup listesini yenile — bottan mesaj alan gruplar listelenir">&#x27F3; Listeyi yenile</button>
903
+ <div id="finDmTgHint" class="fin-set-note" style="margin:2px 0 0"></div>
904
+ </div>
894
905
  </div>
895
906
  </div>
896
907
  </div>
@@ -203,6 +203,11 @@ const els = {
203
203
  finShowNotify: $('#finShowNotify'),
204
204
  finShowPerf: $('#finShowPerf'),
205
205
  finShowFlow: $('#finShowFlow'),
206
+ finDmTgOn: $('#finDmTgOn'),
207
+ finDmTgWrap: $('#finDmTgWrap'),
208
+ finDmTgChat: $('#finDmTgChat'),
209
+ finDmTgRefresh: $('#finDmTgRefresh'),
210
+ finDmTgHint: $('#finDmTgHint'),
206
211
  finRiskCard: $('#finRiskCard'),
207
212
  finNotifyCard: $('#finNotifyCard'),
208
213
  finPerfCard: $('#finPerfCard'),
@@ -10119,6 +10124,11 @@ async function finSnapshot() {
10119
10124
  if (!finView.risk && r.cfg && r.cfg.watchdog !== false) finSaveCfg({ watchdog: false });
10120
10125
  finRenderStats(r.stats, r.equity, r.account && r.account.currency ? r.account.currency : '');
10121
10126
  finRenderAlerts(r.alerts);
10127
+ /* görünüm ayarları açıkken AJAN DM ↔ TELEGRAM seçimi cfg ile senkron kalsın
10128
+ (kullanıcı dropdown'dayken DOM'a dokunulmaz) */
10129
+ if (els.finSettingsOverlay && !els.finSettingsOverlay.hidden && document.activeElement !== els.finDmTgChat) {
10130
+ finDmTgPaint();
10131
+ }
10122
10132
  }
10123
10133
 
10124
10134
  function finLogLine(line) {
@@ -10280,8 +10290,63 @@ function finViewApply() {
10280
10290
  if (!finView.risk) finSaveCfg({ watchdog: false });
10281
10291
  }
10282
10292
 
10293
+ /* ---------- AJAN DM ↔ TELEGRAM (görünüm ayarları) ----------
10294
+ Ayarlar settings.finance'ta: dmTelegram + dmTelegramChat/Title. Grup listesi
10295
+ bottan mesaj alan gruplardan gelir (kanal: tg:groups:list). */
10296
+ let finDmTgGroups = [];
10297
+
10298
+ function finDmTgPaint() {
10299
+ if (!els.finDmTgOn) return;
10300
+ const cfg = finCfgCache || {};
10301
+ const on = cfg.dmTelegram === true;
10302
+ els.finDmTgOn.checked = on;
10303
+ const row = els.finDmTgOn.closest('.fin-set-row');
10304
+ if (row) row.classList.toggle('on', on);
10305
+ if (els.finDmTgWrap) els.finDmTgWrap.hidden = !on;
10306
+ if (els.finDmTgChat) {
10307
+ const cur = String(cfg.dmTelegramChat || '');
10308
+ const sel = els.finDmTgChat;
10309
+ sel.textContent = '';
10310
+ const opt0 = document.createElement('option');
10311
+ opt0.value = '';
10312
+ opt0.textContent = finDmTgGroups.length ? '— Grup seç —' : '— Görünen grup yok —';
10313
+ sel.appendChild(opt0);
10314
+ let has = false;
10315
+ for (const g of finDmTgGroups) {
10316
+ const o = document.createElement('option');
10317
+ o.value = String(g.id);
10318
+ o.textContent = String(g.title || g.id) + ' (' + g.id + ')';
10319
+ if (String(g.id) === cur) { o.selected = true; has = true; }
10320
+ sel.appendChild(o);
10321
+ }
10322
+ /* listelenmemiş kayıtlı grup kaybolmasın — mevcut seçim korunur */
10323
+ if (cur && !has) {
10324
+ const o = document.createElement('option');
10325
+ o.value = cur;
10326
+ o.textContent = String(cfg.dmTelegramTitle || cur) + ' (' + cur + ')';
10327
+ o.selected = true;
10328
+ sel.appendChild(o);
10329
+ }
10330
+ }
10331
+ if (els.finDmTgHint) {
10332
+ els.finDmTgHint.textContent = finDmTgGroups.length
10333
+ ? 'Seçili gruptaki mesajlar AJAN DM grubunda görünür; AJAN DM mesajları da bu gruba düşer.'
10334
+ : 'Botu bir Telegram grubuna ekle, gruba bir mesaj yaz ve "Listeyi yenile"ye bas (BotFather → /setprivacy → Disable ya da bot gruba admin olmalı).';
10335
+ }
10336
+ }
10337
+
10338
+ async function finDmTgRefresh() {
10339
+ try {
10340
+ const list = await beast.tgGroupsList();
10341
+ finDmTgGroups = Array.isArray(list) ? list : [];
10342
+ } catch { finDmTgGroups = []; }
10343
+ finDmTgPaint();
10344
+ }
10345
+
10283
10346
  function finSettingsOpen() {
10284
10347
  if (els.finSettingsOverlay) els.finSettingsOverlay.hidden = false;
10348
+ finDmTgPaint();
10349
+ finDmTgRefresh();
10285
10350
  }
10286
10351
  function finSettingsCloseModal() {
10287
10352
  if (els.finSettingsOverlay) els.finSettingsOverlay.hidden = true;
@@ -10315,6 +10380,30 @@ if (els.finShowRisk) els.finShowRisk.addEventListener('change', () => finViewTog
10315
10380
  if (els.finShowNotify) els.finShowNotify.addEventListener('change', () => finViewToggle('notify', els.finShowNotify.checked));
10316
10381
  if (els.finShowPerf) els.finShowPerf.addEventListener('change', () => finViewToggle('perf', els.finShowPerf.checked));
10317
10382
  if (els.finShowFlow) els.finShowFlow.addEventListener('change', () => finViewToggle('flow', els.finShowFlow.checked));
10383
+ /* AJAN DM ↔ TELEGRAM köprüsü kontrolleri */
10384
+ if (els.finDmTgOn) {
10385
+ els.finDmTgOn.addEventListener('change', () => {
10386
+ const on = els.finDmTgOn.checked;
10387
+ if (finCfgCache) finCfgCache.dmTelegram = on;
10388
+ finSaveCfg({ dmTelegram: on });
10389
+ finDmTgPaint();
10390
+ toast(on ? 'AJAN DM ↔ Telegram köprüsü açıldı' : 'AJAN DM ↔ Telegram köprüsü kapatıldı');
10391
+ if (on) finDmTgRefresh();
10392
+ });
10393
+ }
10394
+ if (els.finDmTgChat) {
10395
+ els.finDmTgChat.addEventListener('change', () => {
10396
+ const id = els.finDmTgChat.value || '';
10397
+ const g = finDmTgGroups.find((x) => String(x.id) === String(id));
10398
+ if (finCfgCache) {
10399
+ finCfgCache.dmTelegramChat = id;
10400
+ finCfgCache.dmTelegramTitle = g ? String(g.title || '') : '';
10401
+ }
10402
+ finSaveCfg({ dmTelegramChat: id, dmTelegramTitle: g ? String(g.title || '') : '' });
10403
+ toast(id ? 'Telegram grubu bağlandı: ' + (g ? g.title || id : id) : 'Telegram grubu seçilmedi');
10404
+ });
10405
+ }
10406
+ if (els.finDmTgRefresh) els.finDmTgRefresh.addEventListener('click', () => finDmTgRefresh());
10318
10407
  finViewApply();
10319
10408
 
10320
10409
  /* ---------- BÖLÜM SIRALAMA (sürükle-bırak) ----------
@@ -5869,3 +5869,23 @@ body.finance-mode #composerWrap { margin-right: var(--finW, 400px); }
5869
5869
  .fin-set-row .fsr-main b { font-size: 12px; color: var(--text); letter-spacing: 0.4px; }
5870
5870
  .fin-set-row .fsr-main i { font-style: normal; font-size: 10.5px; color: var(--muted); }
5871
5871
  .fin-set-note { margin-top: 8px; font-size: 10.5px; color: var(--muted); line-height: 1.4; }
5872
+ .fin-set-sep { height: 1px; margin: 10px 0; background: var(--border); }
5873
+ .fin-set-sub {
5874
+ display: flex;
5875
+ flex-direction: column;
5876
+ gap: 6px;
5877
+ margin: 2px 0 6px;
5878
+ padding: 9px 10px;
5879
+ background: var(--panel);
5880
+ border: 1px solid var(--border);
5881
+ border-radius: 9px;
5882
+ }
5883
+ .fin-set-sub[hidden] { display: none; }
5884
+ .fin-set-lbl { font-size: 10.5px; color: var(--muted); letter-spacing: 0.5px; }
5885
+ .fin-set-sub select { width: 100%; }
5886
+ .fin-set-btn {
5887
+ align-self: flex-start;
5888
+ font-size: 10.5px;
5889
+ padding: 3px 8px;
5890
+ cursor: pointer;
5891
+ }