beast-agent 2.54.0 → 2.56.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.54.0",
4
+ "version": "2.56.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,8 +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_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
+ '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' +
2014
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' +
2015
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' +
2016
2018
  'YETKİLERİN (AÇIK — çekinmeden kullan):\n' +
@@ -6175,7 +6177,8 @@ const TOOLS = [
6175
6177
  windowMin: { type: 'number', description: 'kind=logs: sliding window in minutes (default 10, max 720)' },
6176
6178
  everyMin: { type: 'number', description: 'Check interval in minutes (default 15, min 1, max 1440)' },
6177
6179
  everySec: { type: 'number', description: 'Check interval in SECONDS (10-8640) — overrides everyMin for fast watchers (e.g. 30 = every 30s)' },
6178
- cooldownMin: { type: 'number', description: 'Min minutes between notifications (default 60)' },
6180
+ cooldownMin: { type: 'number', description: 'İki bildirim arası en az dakika (varsayılan 60). >0 ise alarm TEKRARLIDIR: koşul sürdükçe soğuma dolduğunda yeniden bildirir (ör. 5 = 5 dakikada bir hatırlat). 0 = yalnız koşul yeniden sağlandığında (kenar tetikleme).' },
6181
+ once: { type: 'boolean', description: 'true = tek seferlik izleyici: ilk tetiklemede kapanır (varsayılan false = tekrarlı)' },
6179
6182
  },
6180
6183
  required: ['name', 'kind'],
6181
6184
  },
@@ -200,7 +200,7 @@ const definitions = NAMES.map((name) => {
200
200
  },
201
201
  mt5_alerts: {
202
202
  description:
203
- 'FİYAT ALARMI: mt5_alerts {action:"list"|"set"|"remove"}. set: {symbol, price, direction:"above"|"below", note?}. Fiyat tetiklenince sahibine bildirim gider + alarm kapanır. Örn: XAUUSD 3400 üstü / 3280 altı kritik seviye bildirimi kur.',
203
+ 'FİYAT ALARMI: mt5_alerts {action:"list"|"set"|"remove"}. set: {symbol, price, direction:"above"|"below", mode:"once"|"repeat", note?, cooldownMin?}. ALARM MODUNU KURAN AJAN (sen) SEÇERSİN — 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 bildirir + sahibi ajanı uyandırır; cooldownMin varsayılan 5). Örn kritik seviye: XAUUSD 3400 üstü → mode:"repeat", cooldownMin:15; tek kere haber: mode:"once".',
204
204
  parameters: {
205
205
  type: 'object',
206
206
  properties: {
@@ -208,7 +208,14 @@ const definitions = NAMES.map((name) => {
208
208
  symbol: { type: 'string', description: 'set için sembol' },
209
209
  price: { type: 'number', description: 'set için tetik fiyatı' },
210
210
  direction: { type: 'string', enum: ['above', 'below'], description: 'above: fiyat ≥ price; below: fiyat ≤ price' },
211
+ mode: {
212
+ type: 'string',
213
+ enum: ['once', 'repeat'],
214
+ description: "set için ZORUNLU: 'once' tek seferlik | 'repeat' tekrarlı (kararı alarmı kuran ajan verir)",
215
+ },
211
216
  note: { type: 'string', description: 'Alarm notu (bildirimde görünür)' },
217
+ cooldownMin: { type: 'number', description: "mode:'repeat' için iki tetik arası en az dakika (varsayılan 5, 0 = her tetiklemede, max 10080)" },
218
+ once: { type: 'boolean', description: "mode'a alternatif kısa yol (true = tek seferlik)" },
212
219
  id: { type: 'string', description: 'remove için alarm id' },
213
220
  },
214
221
  required: ['action'],
@@ -275,12 +282,16 @@ const definitions = NAMES.map((name) => {
275
282
  },
276
283
  },
277
284
  mt5_close: {
278
- 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.',
279
287
  parameters: {
280
288
  type: 'object',
281
289
  properties: {
282
290
  ticket: { type: 'number', description: 'Pozisyon ticket no' },
283
- 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)' },
284
295
  },
285
296
  required: ['ticket'],
286
297
  },
@@ -425,9 +436,34 @@ const handlers = {
425
436
  if (!symbol) return { ok: false, error: 'symbol gerekli' };
426
437
  if (!isFinite(price) || price <= 0) return { ok: false, error: 'price gerekli (pozitif sayı)' };
427
438
  if (direction !== 'above' && direction !== 'below') return { ok: false, error: "direction 'above' veya 'below' olmalı" };
428
- const alarm = alertsApi.set({ symbol, price, direction, note: String(args.note || '').slice(0, 200), sid: ctx && ctx.sessionId ? String(ctx.sessionId) : '' });
439
+ /* ALARM MODUNU KURAN AJAN SEÇER: 'once' (tek seferlik) ya da
440
+ 'repeat' (tekrarlı; koşul sürdükçe cooldownMin'de bir). Seçim
441
+ yapılmadan alarm kurulamaz — sistem varsayılanına bırakılmaz. */
442
+ const mRaw = String(args.mode || '').toLowerCase().trim();
443
+ let once;
444
+ if (mRaw === 'once' || mRaw === 'tek' || mRaw === 'tek_seferlik' || mRaw === 'tek-seferlik') once = true;
445
+ else if (mRaw === 'repeat' || mRaw === 'recurring' || mRaw === 'tekrarli' || mRaw === 'surekli' || mRaw === 'always') once = false;
446
+ else if (args.once !== undefined) once = args.once === true || String(args.once).toLowerCase() === 'true';
447
+ else {
448
+ return {
449
+ ok: false,
450
+ error:
451
+ "mode gerekli — alarmı kuran ajan karar verir: mode:'once' (tek seferlik, ilk tetiklemede kapanır) ya da mode:'repeat' (tekrarlı; koşul sürdükçe cooldownMin'de bir uyarır, varsayılan 5 dk)",
452
+ };
453
+ }
454
+ const cdRaw = Number(args.cooldownMin);
455
+ const cooldownMin = Number.isFinite(cdRaw) ? Math.min(Math.max(Math.round(cdRaw), 0), 10080) : undefined;
456
+ const alarm = alertsApi.set({
457
+ symbol,
458
+ price,
459
+ direction,
460
+ note: String(args.note || '').slice(0, 200),
461
+ sid: ctx && ctx.sessionId ? String(ctx.sessionId) : '',
462
+ ...(cooldownMin === undefined ? {} : { cooldownMin }),
463
+ once,
464
+ });
429
465
  if (!alarm) return { ok: false, error: 'alarm kurulamadı' };
430
- noteTrade('alert-set', { symbol, price, direction, id: alarm.id, note: alarm.note }, ctx || {});
466
+ noteTrade('alert-set', { symbol, price, direction, id: alarm.id, note: alarm.note, cooldownMin: alarm.cooldownMin, once: alarm.once }, ctx || {});
431
467
  return { ok: true, alert: alarm };
432
468
  }
433
469
  return { ok: false, error: "action: list|set|remove" };
@@ -617,18 +653,48 @@ const handlers = {
617
653
  return { ok: true, opened: { symbol, side, volume: vol, sl, tp }, risk: riskInfo, result: data && data.result };
618
654
  },
619
655
  async mt5_close(args, ctx) {
620
- const cfg = getCfg();
621
656
  if (!mt5.running) return notConnected();
622
657
  const ticket = Number(args.ticket);
623
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));
624
663
  let vol = Number(args.volume) || 0;
625
- if (vol > 0) {
626
- const maxLot = Number(cfg.maxLot) || 0.1;
627
- 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;
628
678
  }
629
- const data = await bcall('close', { ticket, volume: vol || 0 }, 20000);
630
- noteTrade('close', { ticket, volume: vol || 'all', result: data && data.result }, ctx);
631
- 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 };
632
698
  },
633
699
  async mt5_modify(args, ctx) {
634
700
  if (!mt5.running) return notConnected();
@@ -147,6 +147,18 @@ function parseOrderType(raw, fallbackSide) {
147
147
  return { ok: true, side, kind: 'market', type: side + '_market' };
148
148
  }
149
149
 
150
+ /* ---- fiyat alarmı soğuması ----
151
+ Tekrarlı alarm (once=false): aynı koşul sürdükçe en fazla cooldownMin
152
+ dakikada bir tetiklenir. once=true alarmda soğuma uygulanmaz (ilk
153
+ tetiklemede kapanır). */
154
+ function alertCooldownActive(a, now) {
155
+ if (!a || a.once) return false;
156
+ const cd = (Number(a.cooldownMin) || 0) * 60000;
157
+ if (!(cd > 0)) return false;
158
+ const last = Number(a.lastFiredAt) || 0;
159
+ return last > 0 && Number(now) - last < cd;
160
+ }
161
+
150
162
  /* ---- bekleyen emir (pending) → pozisyon eşleştirme ----
151
163
  Watchdog turu emir listesini karşılaştırır: listeden DÜŞEN emir için aynı
152
164
  turda YENİ açılan (ya da netting hesapta hacmi artan) eşleşen bir pozisyon
@@ -216,4 +228,4 @@ function matchPendingDelta(goneOrders, freshPositions) {
216
228
  return out;
217
229
  }
218
230
 
219
- module.exports = { plan, roundTo, stepRound, closeKind, parseOrderType, orderSide, orderTypeLabel, orderVolume, matchPendingDelta };
231
+ module.exports = { plan, roundTo, stepRound, closeKind, parseOrderType, alertCooldownActive, orderSide, orderTypeLabel, orderVolume, matchPendingDelta };
@@ -172,7 +172,9 @@ function normalize(input) {
172
172
  const everyMin = Math.min(Math.max(Math.round(rawMin) || 15, 1), 1440);
173
173
  everySec = everyMin * 60;
174
174
  }
175
- /* 0 geçerlidir (cooldown yok); sadece sayılamayan değerler varsayılana döner */
175
+ /* 0 geçerlidir (cooldown yok → yalnız kenar tetikleme); sayılamayan değerler
176
+ varsayılana döner. cooldownMin > 0 ise alarm TEKRARLIDIR: koşul sürdükçe
177
+ soğuma dolduğunda yeniden bildirir. */
176
178
  const cdRaw = Number(i.cooldownMin ?? i.cooldown_min ?? 60);
177
179
  const cooldownMin = Number.isFinite(cdRaw) ? Math.min(Math.max(Math.round(cdRaw), 0), 10080) : 60;
178
180
  const prompt = String(i.prompt || '').trim().slice(0, 2000);
@@ -192,6 +194,7 @@ function normalize(input) {
192
194
  everyMin: Math.max(1, Math.round(everySec / 60)), // geriye dönük uyum
193
195
  everySec,
194
196
  cooldownMin,
197
+ once: !!i.once, /* tek seferlik: ilk tetiklemede kapanır */
195
198
  prompt,
196
199
  sessionId: i.sessionId || null,
197
200
  enabled: true,
@@ -223,11 +226,15 @@ function compare(op, current, target) {
223
226
  }
224
227
 
225
228
  /* Tek kontrol uygula — saf: tetik kararı + kaydedilecek yama.
226
- Kenar-tetiklemeli: koşul doğruyken tek kez ateşlenir, koşul normale
227
- dönünce yeniden kurulur (armed). */
229
+ TEKRARLI ALARM: cooldownMin > 0 ise koşul sürdükçe, soğuma her dolduğunda
230
+ YENİDEN tetiklenir (tickOnce soğuma boyunca zaten kontrol etmez).
231
+ cooldownMin = 0 → yalnız kenar tetikleme (koşul bozulup yeniden sağlanınca).
232
+ 'changed' her zaman kenar tetiklemelidir (değer değişimi olaydır). */
228
233
  function applyCheck(w, value, now) {
229
234
  const cond = compare(w.op, value, w.value);
230
- const triggered = cond && w.armed;
235
+ const cdMin = Number(w.cooldownMin) || 0;
236
+ const repeat = cdMin > 0 && w.op !== 'changed' && !!w.lastTriggeredAt;
237
+ const triggered = !!(cond && (w.armed || repeat));
231
238
  const patch = {
232
239
  lastCheckAt: new Date(now).toISOString(),
233
240
  lastValue: value === undefined ? null : value,
@@ -371,6 +378,8 @@ async function tickOnce(deps = {}) {
371
378
  w.lastError = '';
372
379
  pushLog(w.id, r.triggered ? 'trigger' : 'ok', value);
373
380
  if (r.triggered) {
381
+ /* TEK SEFERLİK: ilk tetiklemede izleyici kapanır (yeniden etkinleştirilebilir) */
382
+ if (w.once) w.enabled = false;
374
383
  if (typeof hooks.onTrigger === 'function') hooks.onTrigger({ ...w }, value);
375
384
  events.push({ id: w.id, name: w.name, value });
376
385
  }
package/src/main.js CHANGED
@@ -9780,7 +9780,14 @@ async function channelSendFromAgent(input) {
9780
9780
  };
9781
9781
  }
9782
9782
 
9783
- /* ---- fiyat alarmları (kalıcı) ---- */
9783
+ /* ---- fiyat alarmları (kalıcı) ----
9784
+ TEKRARLI (varsayılan): alarm kapanmaz; koşul sürdükçe en fazla
9785
+ cooldownMin dakikada bir tekrar tetikler. once:true → tek tetiklemede
9786
+ kapanır. cooldownMin'i AJAN karar verir (mt5_alerts ile). */
9787
+ const FIN_ALERT_CD_DEFAULT = 5; /* dk */
9788
+ function finAlertCooldown(a) {
9789
+ return Math.min(Math.max(Math.round(Number(a && a.cooldownMin) || FIN_ALERT_CD_DEFAULT), 0), 10080);
9790
+ }
9784
9791
  function finAlertsLoad() {
9785
9792
  const raw = finReadJson(finFile('alerts.json'), []);
9786
9793
  financeState.alerts = (Array.isArray(raw) ? raw : [])
@@ -9791,6 +9798,10 @@ function finAlertsLoad() {
9791
9798
  direction: String((a && a.direction) || 'above').toLowerCase(),
9792
9799
  note: String((a && a.note) || '').slice(0, 200),
9793
9800
  sid: String((a && a.sid) || ''), /* alarmı kuran ajan oturumu (uyandırma hedefi) */
9801
+ once: !!(a && a.once), /* tek seferlik mi */
9802
+ cooldownMin: finAlertCooldown(a),
9803
+ lastFiredAt: Number(a && a.lastFiredAt) || 0,
9804
+ fires: Number(a && a.fires) || 0,
9794
9805
  at: Number(a && a.at) || Date.now(),
9795
9806
  }))
9796
9807
  .filter((a) => a.id && a.symbol && a.price > 0);
@@ -9807,18 +9818,41 @@ function finAlertApi() {
9807
9818
  const symbol = String((a && a.symbol) || '').toUpperCase();
9808
9819
  const price = Number(a && a.price);
9809
9820
  if (!symbol || !(price > 0)) return null;
9821
+ const direction = String((a && a.direction) || 'above').toLowerCase() === 'below' ? 'below' : 'above';
9822
+ /* AYNI ALARM ZATEN AÇIKSA yenisini EKLEME — mevcut kaydı güncelle:
9823
+ ajan her turda aynı seviyeyi yeniden kurup alarm/tur döngüsü
9824
+ (ard arda tetik + uyandırma) üretmesin. lastFiredAt KORUNUR. */
9825
+ const existing = financeState.alerts.find(
9826
+ (x) => x.symbol === symbol && x.direction === direction && Math.abs(Number(x.price) - price) < 1e-9
9827
+ );
9828
+ if (existing) {
9829
+ if (a.note !== undefined) existing.note = String(a.note || '').slice(0, 200);
9830
+ if (a.sid !== undefined) existing.sid = String(a.sid || '');
9831
+ if (a.cooldownMin !== undefined) existing.cooldownMin = finAlertCooldown(a);
9832
+ if (a.once !== undefined) existing.once = !!a.once;
9833
+ finAlertsSave();
9834
+ financeLog(`[alarm] güncellendi (zaten açıktı): ${existing.symbol} ${existing.direction === 'below' ? '≤' : '≥'} ${existing.price}`);
9835
+ return { ...existing, updated: true };
9836
+ }
9810
9837
  const alarm = {
9811
9838
  id: 'a' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
9812
9839
  symbol,
9813
9840
  price,
9814
- direction: String((a && a.direction) || 'above').toLowerCase() === 'below' ? 'below' : 'above',
9841
+ direction,
9815
9842
  note: String((a && a.note) || '').slice(0, 200),
9816
9843
  sid: String((a && a.sid) || ''),
9844
+ once: !!(a && a.once),
9845
+ cooldownMin: finAlertCooldown(a),
9846
+ lastFiredAt: 0,
9847
+ fires: 0,
9817
9848
  at: Date.now(),
9818
9849
  };
9819
9850
  financeState.alerts.push(alarm);
9820
9851
  finAlertsSave();
9821
- financeLog(`[alarm] kuruldu: ${alarm.symbol} ${alarm.direction === 'below' ? '≤' : '≥'} ${alarm.price}`);
9852
+ financeLog(
9853
+ `[alarm] kuruldu: ${alarm.symbol} ${alarm.direction === 'below' ? '≤' : '≥'} ${alarm.price}` +
9854
+ (alarm.once ? ' (tek seferlik)' : ` (tekrarlı · ${alarm.cooldownMin} dk soğuma)`)
9855
+ );
9822
9856
  return alarm;
9823
9857
  },
9824
9858
  remove: (id) => {
@@ -10100,23 +10134,39 @@ async function finCheckAlerts() {
10100
10134
  prices[String(row.symbol)] = { bid: Number(row.bid), ask: Number(row.ask) };
10101
10135
  }
10102
10136
  let changed = false;
10137
+ const now = Date.now();
10103
10138
  for (const a of alerts.slice()) {
10104
10139
  const p = prices[a.symbol];
10105
10140
  if (!p || !isFinite(p.bid)) continue;
10141
+ /* soğuma: tekrarlı alarm koşul sürse bile cooldown dolmadan tekrar etmez */
10142
+ if (finwatch.alertCooldownActive(a, now)) continue;
10106
10143
  const hit = a.direction === 'below' ? p.bid <= a.price : p.bid >= a.price;
10107
10144
  if (!hit) continue;
10108
10145
  changed = true;
10109
- financeState.alerts = financeState.alerts.filter((x) => x.id !== a.id);
10110
- const line = `🔔 ALARM: ${a.symbol} ${a.direction === 'below' ? '≤' : '≥'} ${a.price} (şimdi ${p.bid})${a.note ? ' — ' + a.note : ''}`;
10146
+ const fires = (Number(a.fires) || 0) + 1;
10147
+ if (a.once) {
10148
+ /* tek seferlik: tetiklendi → kapanır */
10149
+ financeState.alerts = financeState.alerts.filter((x) => x.id !== a.id);
10150
+ } else {
10151
+ /* tekrarlı: alarm AÇIK kalır; cooldown sonrası yeniden hatırlatır */
10152
+ a.lastFiredAt = now;
10153
+ a.fires = fires;
10154
+ }
10155
+ const tekrar = fires > 1 ? ` (tekrar #${fires - 1})` : '';
10156
+ const cdTxt = a.once ? ' · tek seferlik' : ` · ${finAlertCooldown(a)} dk soğuma`;
10157
+ const line = `🔔 ALARM${tekrar}: ${a.symbol} ${a.direction === 'below' ? '≤' : '≥'} ${a.price} (şimdi ${p.bid})${a.note ? ' — ' + a.note : ''}${cdTxt}`;
10111
10158
  financeLog('[alarm] ' + line);
10112
10159
  financeNotify(line, 'alert');
10113
- finJournal({ kind: 'alert', symbol: a.symbol, price: a.price, direction: a.direction, note: a.note || '' });
10160
+ finJournal({ kind: 'alert', symbol: a.symbol, price: a.price, direction: a.direction, note: a.note || '', fires, once: !!a.once });
10114
10161
  /* ALARM SAHİBİ AJAN: alarmı kuran koşan finance ajanı DM'den uyanır;
10115
10162
  sahip koşmuyor/biliniyorsa koşan tüm finance ajanlarına düşer. */
10116
10163
  const wakeTxt =
10117
- `[FİNANS OLAYI — FİYAT ALARMI, TUR SANİYESİ BEKLENMEDEN İLETİLDİ]\n` +
10118
- `- ${a.symbol} ${a.direction === 'below' ? '≤' : '≥'} ${a.price} tetiklendi (şimdi ${p.bid})${a.note ? ' — ' + a.note : ''}\n` +
10119
- `Şimdi yap: alarmı kurma nedenini hatırla; fiyat seviyesini ve planını değerlendir, gerekiyorsa işlem/uyarı üret. Kısa rapor ver.`;
10164
+ `[FİNANS OLAYI — FİYAT ALARMI${tekrar}, TUR SANİYESİ BEKLENMEDEN İLETİLDİ]\n` +
10165
+ `- ${a.symbol} ${a.direction === 'below' ? '≤' : '≥'} ${a.price} tetiklendi (şimdi ${p.bid})${a.note ? ' — ' + a.note : ''}${cdTxt}\n` +
10166
+ (a.once
10167
+ ? 'Alarm TEK SEFERLİKTİ ve kapandı. '
10168
+ : `Alarm TEKRARLI açık kalıyor${a.cooldownMin > 0 ? ` — koşul sürerse en erken ${a.cooldownMin} dk sonra yeniden uyarır` : ''}. `) +
10169
+ 'Şimdi yap: alarmı kurma nedenini hatırla; fiyat seviyesini ve planını değerlendir, gerekiyorsa işlem/uyarı üret. Kısa rapor ver.';
10120
10170
  if (!finWakeAgentDm(a.sid, wakeTxt)) finWakeAgents(wakeTxt, { kind: 'alarm', symbol: a.symbol });
10121
10171
  }
10122
10172
  if (changed) finAlertsSave();
@@ -10264,15 +10314,24 @@ async function finWatchTick() {
10264
10314
  finJournal({ kind: 'watchdog', action: 'sl-move', ticket, symbol: p.symbol, sl: act.sl });
10265
10315
  }
10266
10316
  } else if (act.kind === 'partial') {
10267
- const r = await mt5bridge.call('close', { ticket: p.ticket, volume: act.volume }, 15000).catch(() => null);
10268
- if (r && r.ok) {
10269
- st.partial = true;
10270
- const line = `🎯 Kısmi TP: ${p.symbol} #${ticket} ${act.volume} lot kapatıldı`;
10271
- financeLog('[watchdog] ' + line);
10272
- if (cfg.notifyWatchdog !== false) financeNotify(line, 'watchdog');
10273
- finJournal({ kind: 'watchdog', action: 'partial-tp', ticket, symbol: p.symbol, volume: act.volume });
10274
- finStatsRefresh(true);
10275
- }
10317
+ /* OTOMATİK KAPATMA YOK — kısmi TP/stop kararını AJAN verir: seviye
10318
+ gelince ajanlar TUR BEKLEMEDEN uyandırılır; uygun görürse
10319
+ mt5_close {percent, kind:"partial_tp"} ile kısmi kâr alır. */
10320
+ st.partial = true;
10321
+ const rNow = decision.r > 0
10322
+ ? 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
10323
+ : 0;
10324
+ const pct = Number(cfg.partialPct) || 50;
10325
+ const line = `🎯 KISMİ TP FIRSATI: ${p.symbol} #${ticket} +${rNow}R (önerilen %${pct}) — kapatma kararı ajanın`;
10326
+ financeLog('[watchdog] ' + line);
10327
+ if (cfg.notifyWatchdog !== false) financeNotify(line, 'watchdog');
10328
+ finJournal({ kind: 'watchdog', action: 'partial-tp-opportunity', ticket, symbol: p.symbol, r: rNow, pct });
10329
+ finWakeAgents(
10330
+ `[FİNANS OLAYI — KISMİ TP FIRSATI, TUR BEKLEMEDEN İLETİLDİ]\n` +
10331
+ `- ${p.symbol}${Number(p.type) === 0 ? ' BUY' : ' SELL'} #${ticket} +${rNow}R seviyesinde (önerilen kısmi kapatma: %${pct})\n` +
10332
+ `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.`,
10333
+ { kind: 'partial-tp', ticket }
10334
+ );
10276
10335
  } else if (act.kind === 'warnSL') {
10277
10336
  st.warnSL = true;
10278
10337
  const line = `⚠️ SL yaklaşıyor: ${p.symbol} #${ticket} (SL ${p.sl}, fiyat ${p.price_current})`;
@@ -10480,6 +10539,19 @@ function finTradeEvent(kRaw, dataRaw, sidRaw) {
10480
10539
  if (k === 'trade') {
10481
10540
  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 || '' });
10482
10541
  } else if (k === 'close') {
10542
+ /* KISMİ KAPATMA (kısmi TP / kısmi stop): ajan kararı; günlük + bildirim.
10543
+ Kesin toplam K/Z yine watchdog tarafından yazılır (çift sayım yok). */
10544
+ const isPartial = d.partial === true || Number(d.percent) > 0;
10545
+ if (isPartial) {
10546
+ const ck = String(d.closeKind || '');
10547
+ const label = ck === 'partial_sl' ? 'KISMİ STOP (zarar kes)' : ck === 'partial_tp' ? 'KISMİ TP (kâr al)' : 'KISMİ KAPATMA';
10548
+ finJournal({ kind: 'partial-close', sid, agent: who, ticket: d.ticket, volume: d.volume, percent: Number(d.percent) || 0, closeKind: ck, reason: d.reason || '' });
10549
+ const line = `✂️ ${label}: ticket ${d.ticket}${Number(d.percent) > 0 ? ` · %${Number(d.percent)}` : ''}${d.volume ? ` · ${d.volume} lot` : ''}${who ? ' · ' + who : ''}`;
10550
+ financeLog('[ajan] ' + line);
10551
+ finPush('trade', { line });
10552
+ if (finCfg().notifyTrades !== false) financeNotify(line, 'close', false);
10553
+ return;
10554
+ }
10483
10555
  finJournal({ kind: 'close-req', sid, agent: who, ticket: d.ticket, volume: d.volume });
10484
10556
  return; /* kesin kapanış K/Z'si watchdog tarafından yazılır (çift bildirim yok) */
10485
10557
  } else if (k === 'modify') {
@@ -10532,7 +10604,16 @@ try {
10532
10604
  } else if (kind === 'modify') finTradeEvent('modify', { ticket: a.ticket, sl: a.sl, tp: a.tp }, sid);
10533
10605
  else if (kind === 'pending') finTradeEvent('pending', { symbol: a.symbol, type: a.type, volume: a.volume, reason: a.reason || '' }, sid);
10534
10606
  else if (kind === 'cancel') finTradeEvent('cancel', { ticket: a.ticket }, sid);
10535
- else if (kind === 'close') finTradeEvent('close', { ticket: a.ticket, volume: a.volume || 'all' }, sid);
10607
+ else if (kind === 'close') {
10608
+ finTradeEvent('close', {
10609
+ ticket: a.ticket,
10610
+ volume: a.volume || (a.percent ? 0 : 'all'),
10611
+ percent: Number(a.percent) || 0,
10612
+ partial: !!(Number(a.percent) > 0 || (a.volume && String(a.volume) !== 'all')),
10613
+ closeKind: String(a.kind || a.closeKind || ''),
10614
+ reason: String(a.reason || ''),
10615
+ }, sid);
10616
+ }
10536
10617
  });
10537
10618
  } catch {}
10538
10619
 
@@ -10988,7 +11069,7 @@ function finTraderBrief(agent) {
10988
11069
  `Tur aralığı: ${f.intervalSec} sn · Max lot: ${f.maxLot} · Max eşzamanlı pozisyon: ${f.maxPositions}`,
10989
11070
  roleDef
10990
11071
  ? `UZMANLIK: ${roleDef.desc} — raporlarını bu çerçevede yaz; İŞLEM AÇMA, yalnız analiz + net öneri üret.`
10991
- : '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). Lot için mt5_risksize hesapla ya da mt5_trade’e riskPct ver; SL/TP broker stops_level mesafesine uymalı.',
11072
+ : '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ı.',
10992
11073
  roleDef
10993
11074
  ? '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.'
10994
11075
  : f.strategy ? `Sahibinin strateji notu: ${f.strategy}` : 'Strateji notu yok: trend + destek/direnç + momentum ile temel okuma yap.',
@@ -11265,6 +11346,17 @@ ipcMain.handle('finance:snapshot', async () => {
11265
11346
  };
11266
11347
  });
11267
11348
 
11349
+ ipcMain.handle('finance:alerts', async () => {
11350
+ /* İzleyiciler paneli hafif alarm listesi (hesap/pozisyon çekmez) */
11351
+ return { ok: true, alerts: (financeState.alerts || []).slice(0, 200) };
11352
+ });
11353
+
11354
+ ipcMain.handle('finance:alerts:remove', async (_e, id) => {
11355
+ const ok = finAlertApi().remove(String(id || ''));
11356
+ finPush('alert', { removed: ok, id: String(id || '') });
11357
+ return { ok };
11358
+ });
11359
+
11268
11360
  ipcMain.handle('finance:mode', async (_e, payload) => {
11269
11361
  financeState.mode = !!(payload && payload.on);
11270
11362
  /* BEAST FINANCE = MT5 KAPISI:
package/src/preload.js CHANGED
@@ -117,6 +117,8 @@ contextBridge.exposeInMainWorld('beast', {
117
117
 
118
118
  financeState: () => ipcRenderer.invoke('finance:state'),
119
119
  financeSnapshot: () => ipcRenderer.invoke('finance:snapshot'),
120
+ financeAlertsList: () => ipcRenderer.invoke('finance:alerts'),
121
+ financeAlertRemove: (id) => ipcRenderer.invoke('finance:alerts:remove', id),
120
122
  financeMode: (on, sessionId) => ipcRenderer.invoke('finance:mode', { on, sessionId }),
121
123
  financeSettings: (patch) => ipcRenderer.invoke('finance:settings', patch),
122
124
  financeTraderStart: () => ipcRenderer.invoke('finance:trader:start'),
@@ -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">
@@ -798,6 +798,10 @@
798
798
  <input id="wAddWindow" class="inp" type="number" min="1" max="720" value="10" title="pencere (dk)" style="width:80px" />
799
799
  <input id="wAddRe" class="inp" data-i18n-ph="w_add_re" placeholder="regex (opsiyonel)" />
800
800
  </div>
801
+ <div class="mini-add-row">
802
+ <input id="wAddCd" class="inp" type="number" min="0" max="10080" value="60" title="soğuma (dk): koşul sürdükçe en fazla bu aralıkta bir bildirir — 0 = yalnız koşul yeniden sağlanınca" style="width:70px" />
803
+ <label style="display:flex;align-items:center;gap:5px;font-size:12px;color:var(--muted);white-space:nowrap"><input id="wAddOnce" type="checkbox" /> tek seferlik</label>
804
+ </div>
801
805
  </div>
802
806
  <div id="watchList" class="mini-list"></div>
803
807
  </div>
@@ -6640,17 +6640,35 @@ function fmtWhen(iso) {
6640
6640
  Kompakt liste: durum noktası + ad + bir sonraki tetik; satır → izleyici
6641
6641
  modalı; ▶/× mini kontroller. Yalnız agent modunda görünür (CSS). */
6642
6642
  let lastWatchPaneKey = '';
6643
+ /* FİYAT ALARMLARI (Beast Finance): İzleyiciler paneli + modalında watcher'larla
6644
+ BİRLİKTE listelenir — tüm alarmlar tek yerde görünür/silinir. */
6645
+ function finAlertMeta(a) {
6646
+ const dir = a.direction === 'below' ? '≤' : '≥';
6647
+ return {
6648
+ name: `🔔 ${a.symbol} ${dir} ${a.price}`,
6649
+ meta:
6650
+ (a.once ? 'tek seferlik' : `her ${Number(a.cooldownMin) || 5} dk`) +
6651
+ (a.fires ? ` · ${a.fires}× tetiklendi` : '') +
6652
+ (a.note ? ` · ${String(a.note).slice(0, 40)}` : ''),
6653
+ };
6654
+ }
6655
+
6643
6656
  async function renderWatchersPane(force) {
6644
6657
  const list = els.watchPaneList;
6645
6658
  if (!list) return;
6646
6659
  if (document.body.classList.contains('ide-mode') || document.body.classList.contains('studio-mode') || document.body.classList.contains('sandbox-mode')) return;
6647
6660
  let rows = [];
6648
6661
  try { rows = (await beast.watchersList()) || []; } catch {}
6649
- const key = JSON.stringify(rows);
6662
+ let alerts = [];
6663
+ try {
6664
+ const ar = await beast.financeAlertsList();
6665
+ alerts = (ar && ar.alerts) || [];
6666
+ } catch {}
6667
+ const key = JSON.stringify([rows, alerts]);
6650
6668
  if (!force && key === lastWatchPaneKey) return;
6651
6669
  lastWatchPaneKey = key;
6652
6670
  list.innerHTML = '';
6653
- if (!rows.length) {
6671
+ if (!rows.length && !alerts.length) {
6654
6672
  list.innerHTML = '<div class="wp-empty">' + _t('w_empty') + '</div>';
6655
6673
  return;
6656
6674
  }
@@ -6698,18 +6716,52 @@ async function renderWatchersPane(force) {
6698
6716
  row.addEventListener('click', () => openWatchModal());
6699
6717
  list.appendChild(row);
6700
6718
  }
6719
+ /* Beast Finance FİYAT ALARMLARI: aynı listede — silinebilir, canlı cd/fire bilgisi */
6720
+ for (const a of alerts) {
6721
+ const info = finAlertMeta(a);
6722
+ const row = document.createElement('div');
6723
+ row.className = 'wp-row alarm';
6724
+ row.title = info.name + (a.note ? ' — ' + a.note : '') + ' · ' + info.meta;
6725
+ row.innerHTML =
6726
+ `<span class="wp-dot"></span>` +
6727
+ `<div class="wp-main">` +
6728
+ `<div class="wp-name">${escapeHtml(info.name)}</div>` +
6729
+ `<div class="wp-meta">${escapeHtml(info.meta)}</div>` +
6730
+ `</div>`;
6731
+ const btns = document.createElement('div');
6732
+ btns.className = 'wp-btns';
6733
+ const d = document.createElement('button');
6734
+ d.className = 'wp-btn del';
6735
+ d.textContent = '×';
6736
+ d.title = _t('cr_del');
6737
+ d.addEventListener('click', async (e) => {
6738
+ e.stopPropagation();
6739
+ await beast.financeAlertRemove(a.id);
6740
+ lastWatchPaneKey = '';
6741
+ renderWatchersPane(true);
6742
+ renderWatchersModal(true);
6743
+ });
6744
+ btns.appendChild(d);
6745
+ row.appendChild(btns);
6746
+ list.appendChild(row);
6747
+ }
6701
6748
  }
6702
6749
 
6703
6750
  let lastWatchKey = '';
6704
6751
  async function renderWatchersModal(force) {
6705
6752
  let rows = []; try { rows = (await beast.watchersList()) || []; } catch {}
6753
+ let alerts = [];
6754
+ try {
6755
+ const ar = await beast.financeAlertsList();
6756
+ alerts = (ar && ar.alerts) || [];
6757
+ } catch {}
6706
6758
  /* canlı yenilemede içerik değişmediyse DOM'a dokunma — buton tıklamaları
6707
6759
  yeniden kurulurken yutulmasın */
6708
- const key = JSON.stringify(rows);
6760
+ const key = JSON.stringify([rows, alerts]);
6709
6761
  if (!force && key === lastWatchKey) return;
6710
6762
  lastWatchKey = key;
6711
6763
  els.watchList.innerHTML = '';
6712
- if (!rows.length) {
6764
+ if (!rows.length && !alerts.length) {
6713
6765
  els.watchList.innerHTML = '<div class="mini-empty">' + _t('w_empty') + '</div>';
6714
6766
  return;
6715
6767
  }
@@ -6724,7 +6776,7 @@ async function renderWatchersModal(force) {
6724
6776
  `<span class="mr-dot"></span>` +
6725
6777
  `<div class="mr-main">` +
6726
6778
  `<div class="mr-name">${escapeHtml(w.name)}</div>` +
6727
- `<div class="mr-meta">[${w.kind}] ${escapeHtml(String(target).slice(0, 60))} · <b>${opText(w)}</b> · her <b>${fmtEvery(w)}</b> · cd ${w.cooldownMin}dk` +
6779
+ `<div class="mr-meta">[${w.kind}] ${escapeHtml(String(target).slice(0, 60))} · <b>${opText(w)}</b> · her <b>${fmtEvery(w)}</b> · ${w.once ? '<b>tek seferlik</b>' : 'cd ' + w.cooldownMin + 'dk'}` +
6728
6780
  (w.lastValue !== null && w.lastValue !== undefined ? ` · son: <b>${escapeHtml(String(w.lastValue).slice(0, 20))}</b>` : '') +
6729
6781
  (w.lastError ? ` · <span style="color:var(--err)">${escapeHtml(w.lastError.slice(0, 40))}</span>` : '') +
6730
6782
  `</div></div>`;
@@ -6749,6 +6801,34 @@ async function renderWatchersModal(force) {
6749
6801
  row.appendChild(btns);
6750
6802
  els.watchList.appendChild(row);
6751
6803
  }
6804
+ /* Beast Finance FİYAT ALARMLARI: modalda da watcher'larla birlikte */
6805
+ for (const a of alerts) {
6806
+ const info = finAlertMeta(a);
6807
+ const row = document.createElement('div');
6808
+ row.className = 'mini-row alarm';
6809
+ row.innerHTML =
6810
+ `<span class="mr-dot"></span>` +
6811
+ `<div class="mr-main">` +
6812
+ `<div class="mr-name">${escapeHtml(info.name)}</div>` +
6813
+ `<div class="mr-meta">${escapeHtml(info.meta)}` +
6814
+ (a.note ? ` · ${escapeHtml(String(a.note).slice(0, 50))}` : '') +
6815
+ `</div></div>`;
6816
+ const btns = document.createElement('div');
6817
+ btns.style.cssText = 'display:flex;gap:6px;flex:none';
6818
+ const d = document.createElement('button');
6819
+ d.className = 'mr-btn del';
6820
+ d.textContent = '×';
6821
+ d.title = _t('cr_del');
6822
+ d.addEventListener('click', async () => {
6823
+ await beast.financeAlertRemove(a.id);
6824
+ lastWatchKey = '';
6825
+ renderWatchersModal(true);
6826
+ renderWatchersPane(true);
6827
+ });
6828
+ btns.appendChild(d);
6829
+ row.appendChild(btns);
6830
+ els.watchList.appendChild(row);
6831
+ }
6752
6832
  }
6753
6833
 
6754
6834
  /* ---------- İZLEYİCİ LOG MODALI ----------
@@ -7801,6 +7881,8 @@ async function init() {
7801
7881
  name: $('#wAddName').value.trim(),
7802
7882
  kind,
7803
7883
  everyMin: Number($('#wAddEvery').value) || 15,
7884
+ cooldownMin: Math.max(0, Number($('#wAddCd') ? $('#wAddCd').value : 60) || 0),
7885
+ once: !!($('#wAddOnce') && $('#wAddOnce').checked),
7804
7886
  };
7805
7887
  if (kind === 'web') {
7806
7888
  input.url = $('#wAddUrl').value.trim();
@@ -7823,6 +7905,8 @@ async function init() {
7823
7905
  const el = $(id);
7824
7906
  if (el) el.value = '';
7825
7907
  }
7908
+ if ($('#wAddOnce')) $('#wAddOnce').checked = false;
7909
+ if ($('#wAddCd')) $('#wAddCd').value = '60';
7826
7910
  lastWatchKey = '';
7827
7911
  lastWatchPaneKey = '';
7828
7912
  await renderWatchersModal(true);
@@ -9979,6 +10063,7 @@ function finRenderAlerts(list) {
9979
10063
  row.innerHTML =
9980
10064
  '<span class="fa-sym">' + escapeHtml(String(a.symbol || '?')) + '</span>' +
9981
10065
  '<span class="fa-cond">' + (a.direction === 'below' ? '≤' : '≥') + ' ' + (Number(a.price) || 0) + '</span>' +
10066
+ '<span class="fa-note">' + (a.once ? 'tek seferlik' : `her ${Number(a.cooldownMin) || 5} dk`) + (a.fires ? ` · ${a.fires}×` : '') + '</span>' +
9982
10067
  (a.note ? '<span class="fa-note">' + escapeHtml(String(a.note)) + '</span>' : '');
9983
10068
  box.appendChild(row);
9984
10069
  }
@@ -1504,6 +1504,8 @@ body.browser-open .mini-overlay { right: var(--bw, 480px); }
1504
1504
  .mini-row.off .mr-name, .mini-row.off .mr-meta { opacity: 0.45; }
1505
1505
  .mr-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); flex: none; }
1506
1506
  .mini-row.off .mr-dot { background: var(--muted); }
1507
+ /* Beast Finance fiyat alarmları: izleyici listesinde turuncu nokta ile ayrılır */
1508
+ .wp-row.alarm .wp-dot, .mini-row.alarm .mr-dot { background: #e6a117; }
1507
1509
  .mr-main { flex: 1; min-width: 0; }
1508
1510
  .mr-name { font-weight: 700; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1509
1511
  .mr-meta { color: var(--muted); font-size: 11px; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }