beast-agent 2.32.0 → 2.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.32.0",
4
+ "version": "2.33.1",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -5477,7 +5477,7 @@ const skills = require('./skills');
5477
5477
  ) {
5478
5478
  return JSON.stringify({ ok: false, error: `unknown tool ${name}` });
5479
5479
  }
5480
- return await tools.exec(name, args, { cwd: this._sessionWorkspace(sessionId), signal, wantDiff: true });
5480
+ return await tools.exec(name, args, { cwd: this._sessionWorkspace(sessionId), signal, wantDiff: true, sessionId });
5481
5481
  } catch (e) {
5482
5482
  return JSON.stringify({ ok: false, error: String((e && e.message) || e) });
5483
5483
  }
@@ -6,14 +6,22 @@
6
6
  main tarafı setConfig/setNotify ile ayar ve bildirim kancasını enjekte eder. */
7
7
 
8
8
  const mt5 = require('../mt5bridge');
9
+ const finindicators = require('./finindicators');
10
+ const finstats = require('./finstats');
11
+ const finrisk = require('./finrisk');
9
12
 
10
13
  const NAMES = [
11
14
  'mt5_status',
12
15
  'mt5_account',
13
16
  'mt5_market',
17
+ 'mt5_rates',
18
+ 'mt5_indicators',
19
+ 'mt5_risksize',
14
20
  'mt5_positions',
15
21
  'mt5_orders',
16
22
  'mt5_history',
23
+ 'mt5_alerts',
24
+ 'mt5_note',
17
25
  'mt5_trade',
18
26
  'mt5_close',
19
27
  'mt5_modify',
@@ -23,6 +31,12 @@ const NAMES = [
23
31
 
24
32
  let getCfg = () => ({ allowTrading: false, maxLot: 0.1, maxPositions: 3, symbols: [] });
25
33
  let notify = () => {};
34
+ /* alarm deposu main süreçte yaşar (dosyaya kalıcı) — buradan enjekte edilir */
35
+ let alertsApi = {
36
+ list: () => [],
37
+ set: () => null,
38
+ remove: () => false,
39
+ };
26
40
 
27
41
  function setConfig(fn) {
28
42
  if (typeof fn === 'function') getCfg = fn;
@@ -32,10 +46,8 @@ function setNotify(fn) {
32
46
  if (typeof fn === 'function') notify = fn;
33
47
  }
34
48
 
35
- function clampLot(v, maxLot) {
36
- const n = Number(v);
37
- if (!isFinite(n) || n <= 0) return 0;
38
- return Math.min(n, maxLot);
49
+ function setAlerts(api) {
50
+ if (api && typeof api === 'object') alertsApi = api;
39
51
  }
40
52
 
41
53
  async function bcall(method, params, timeoutMs) {
@@ -44,8 +56,43 @@ async function bcall(method, params, timeoutMs) {
44
56
  return r.data;
45
57
  }
46
58
 
47
- function noteTrade(kind, data) {
48
- try { notify({ kind, data, at: Date.now() }); } catch {}
59
+ async function symInfoRow(symbol) {
60
+ const data = await bcall('symbols', { symbols: [String(symbol || '').toUpperCase()] }, 10000);
61
+ const row = data && data.symbols && data.symbols[0];
62
+ return row && !row.missing ? row : null;
63
+ }
64
+
65
+ function tickPrice(row, side) {
66
+ const p = side === 'buy' ? Number(row && row.ask) : Number(row && row.bid);
67
+ if (isFinite(p) && p > 0) return p;
68
+ return Number(row && row.bid) || Number(row && row.ask) || 0;
69
+ }
70
+
71
+ /* İşlem öncesi risk katmanı: stops_level + yoğunluk + marj kalkanı.
72
+ Hata metni ya da null döner. positions çağıran tarafından verilir. */
73
+ async function preTradeCheck(cfg, side, symbol, info, price, volume, sl, tp, positions) {
74
+ const closeErr = finrisk.stopsTooClose(info, price, sl, tp);
75
+ if (closeErr) return closeErr;
76
+ const expErr = finrisk.exposureCheck(positions || [], side, symbol, cfg.maxPerSymbol, cfg.maxSameSide);
77
+ if (expErr) return expErr;
78
+ const minLevel = Number(cfg.minMarginLevel) || 0;
79
+ let account = null;
80
+ let estMargin = 0;
81
+ try {
82
+ const a = await bcall('account', {}, 8000);
83
+ account = a && a.account;
84
+ } catch {}
85
+ try {
86
+ const m = await bcall('margin', { symbol, side, volume, price }, 8000);
87
+ estMargin = Number(m && m.margin) || 0;
88
+ } catch {}
89
+ const mErr = finrisk.marginCheck(account, estMargin, minLevel);
90
+ if (mErr && mErr.error) return mErr.error;
91
+ return null;
92
+ }
93
+
94
+ function noteTrade(kind, data, ctx) {
95
+ try { notify({ kind, data, at: Date.now(), sid: ctx && ctx.sessionId ? String(ctx.sessionId) : '' }); } catch {}
49
96
  }
50
97
 
51
98
  function notConnected() {
@@ -66,7 +113,7 @@ const definitions = NAMES.map((name) => {
66
113
  },
67
114
  mt5_market: {
68
115
  description:
69
- 'Verilen semboller için canlı fiyat: bid/ask, spread, digit, point. symbols verilmezse izleme listesi (watchlist) kullanılır. Örn: ["EURUSD","XAUUSD"].',
116
+ 'Verilen semboller için canlı fiyat + TAM sembol sözleşme bilgisi: bid/ask, spread, digit, point, swap_long/short, komisyon, kontrat büyüklüğü, min/max/step lot, stops_level, işlem modu. symbols verilmezse izleme listesi (watchlist) kullanılır. Örn: ["EURUSD","XAUUSD"].',
70
117
  parameters: {
71
118
  type: 'object',
72
119
  properties: {
@@ -74,6 +121,54 @@ const definitions = NAMES.map((name) => {
74
121
  },
75
122
  },
76
123
  },
124
+ mt5_rates: {
125
+ description:
126
+ 'OHLC MUM VERİSİ: mt5_rates {symbol, timeframe:"M1|M5|M15|M30|H1|H4|D1|W1|MN1", count?}. Trend/yapı/destek-direnç analizinin HAM verisi. Varsayılan M15 × 200 mum. Formasyon/price-action için İDEAL: H1/H4 yapı + M15/M5 giriş zamanlaması.',
127
+ parameters: {
128
+ type: 'object',
129
+ properties: {
130
+ symbol: { type: 'string', description: 'Örn: XAUUSD' },
131
+ timeframe: { type: 'string', description: 'M1|M5|M15|M30|H1|H4|D1|W1|MN1 (varsayılan M15)' },
132
+ count: { type: 'number', description: 'Mum sayısı 1-1000 (varsayılan 200)' },
133
+ },
134
+ required: ['symbol'],
135
+ },
136
+ },
137
+ mt5_indicators: {
138
+ description:
139
+ 'TEKNİK GÖSTERGELER: mt5_indicators {symbol, timeframe, count?, indicators?}. indicators örn: ["EMA(50)","EMA(200)","RSI(14)","ATR(14)","MACD(12,26,9)","BB(20,2)","STOCH(14,3,3)","SMA(200)"]. Varsayılan: EMA20/EMA50/RSI14/ATR14/MACD. ATR ile SL mesafesi/lot, RSI ile aşırı alım-satım, EMA ile trend yönü oku.',
140
+ parameters: {
141
+ type: 'object',
142
+ properties: {
143
+ symbol: { type: 'string' },
144
+ timeframe: { type: 'string', description: 'M1|M5|M15|M30|H1|H4|D1|W1|MN1 (varsayılan H1)' },
145
+ count: { type: 'number', description: 'Hesaplamaya giren mum sayısı (varsayılan 300, en fazla 1000)' },
146
+ indicators: {
147
+ type: 'array',
148
+ items: { type: 'string' },
149
+ description: 'Gösterge listesi (boşsa varsayılan set)',
150
+ },
151
+ },
152
+ required: ['symbol'],
153
+ },
154
+ },
155
+ mt5_risksize: {
156
+ description:
157
+ 'RİSK BAZLI LOT HESABI: mt5_risksize {symbol, sl?, entry?, side?, riskPct?, atrMult?, timeframe?}. Bakiyenin riskPct%’i kadar kayıp verecek lotu hesaplar (SL mesafesi × tick değeri). sl yoksa atrMult verilirse ATR(14) ile SL mesafesi önerir. riskPct boşsa ayardaki işlem riski (%) kullanılır. İŞLEM AÇMADAN ÖNCE çağır; çıkan lotu mt5_trade’e ver.',
158
+ parameters: {
159
+ type: 'object',
160
+ properties: {
161
+ symbol: { type: 'string' },
162
+ sl: { type: 'number', description: 'Stop loss fiyatı' },
163
+ entry: { type: 'number', description: 'Giriş fiyatı (boşsa güncel fiyat)' },
164
+ side: { type: 'string', enum: ['buy', 'sell'], description: 'Yön (varsayılan buy)' },
165
+ riskPct: { type: 'number', description: 'Hesap riski yüzdesi (boşsa ayar)' },
166
+ atrMult: { type: 'number', description: 'SL yoksa: SL mesafesi = atrMult × ATR(14)' },
167
+ timeframe: { type: 'string', description: 'ATR zaman dilimi (varsayılan H1)' },
168
+ },
169
+ required: ['symbol'],
170
+ },
171
+ },
77
172
  mt5_positions: {
78
173
  description: 'Açık pozisyonlar: ticket, sembol, yön, lot, açılış fiyatı, güncel fiyat, SL/TP, yüzen kâr/zarar, açılış zamanı.',
79
174
  parameters: { type: 'object', properties: {} },
@@ -89,6 +184,36 @@ const definitions = NAMES.map((name) => {
89
184
  properties: { days: { type: 'number', description: 'Kaç gün geriye bakılsın (varsayılan 1, en fazla 90)' } },
90
185
  },
91
186
  },
187
+ mt5_alerts: {
188
+ description:
189
+ '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.',
190
+ parameters: {
191
+ type: 'object',
192
+ properties: {
193
+ action: { type: 'string', enum: ['list', 'set', 'remove'] },
194
+ symbol: { type: 'string', description: 'set için sembol' },
195
+ price: { type: 'number', description: 'set için tetik fiyatı' },
196
+ direction: { type: 'string', enum: ['above', 'below'], description: 'above: fiyat ≥ price; below: fiyat ≤ price' },
197
+ note: { type: 'string', description: 'Alarm notu (bildirimde görünür)' },
198
+ id: { type: 'string', description: 'remove için alarm id' },
199
+ },
200
+ required: ['action'],
201
+ },
202
+ },
203
+ mt5_note: {
204
+ description:
205
+ 'İŞLEM GÜNLÜĞÜ NOTU: mt5_note {symbol, note, ticket?, side?}. Kararın GEREKÇESİNİ kalıcı günlüğe yaz (neden girdin/çıktın, tez, iptal koşulu). Haftalık performans raporu bu notları içerir — her önemli kararda kullan.',
206
+ parameters: {
207
+ type: 'object',
208
+ properties: {
209
+ symbol: { type: 'string' },
210
+ note: { type: 'string', description: 'Gerekçe/tez (kısa, net)' },
211
+ ticket: { type: 'number', description: 'İlgili pozisyon ticket no (varsa)' },
212
+ side: { type: 'string', enum: ['buy', 'sell', 'wait'] },
213
+ },
214
+ required: ['symbol', 'note'],
215
+ },
216
+ },
92
217
  mt5_trade: {
93
218
  description:
94
219
  'PİYASA EMRİ AÇAR: mt5_trade {symbol, side:"buy"|"sell", volume, sl?, tp?, comment?}. Lot limiti ve max pozisyon sayısı sistem tarafından zorlanır. Otomatik işlem anahtarı kapalıysa reddedilir. SL/TP vermek ŞIDDETLİ önerilir.',
@@ -175,6 +300,31 @@ const handlers = {
175
300
  const data = await bcall('symbols', { symbols: syms }, 10000);
176
301
  return { ok: true, symbols: data && data.symbols ? data.symbols : [] };
177
302
  },
303
+ async mt5_rates(args) {
304
+ if (!mt5.running) return notConnected();
305
+ const symbol = String(args.symbol || '').trim().toUpperCase();
306
+ if (!symbol) return { ok: false, error: 'symbol gerekli' };
307
+ const timeframe = String(args.timeframe || 'M15').trim().toUpperCase();
308
+ let count = Math.round(Number(args.count) || 200);
309
+ count = Math.max(1, Math.min(1000, count));
310
+ const data = await bcall('rates', { symbol, timeframe, count }, 15000);
311
+ const rates = (data && data.rates) || [];
312
+ return { ok: true, symbol, timeframe, count: rates.length, rates };
313
+ },
314
+ async mt5_indicators(args) {
315
+ if (!mt5.running) return notConnected();
316
+ const symbol = String(args.symbol || '').trim().toUpperCase();
317
+ if (!symbol) return { ok: false, error: 'symbol gerekli' };
318
+ const timeframe = String(args.timeframe || 'H1').trim().toUpperCase();
319
+ let count = Math.round(Number(args.count) || 300);
320
+ count = Math.max(30, Math.min(1000, count));
321
+ const specs = Array.isArray(args.indicators) ? args.indicators.map((s) => String(s || '').trim()).filter(Boolean) : [];
322
+ const data = await bcall('rates', { symbol, timeframe, count }, 15000);
323
+ const rates = (data && data.rates) || [];
324
+ if (!rates.length) return { ok: false, error: 'mum verisi yok: ' + symbol + ' ' + timeframe };
325
+ const summary = finindicators.compute(rates, specs);
326
+ return { ok: true, symbol, timeframe, ...summary };
327
+ },
178
328
  async mt5_positions() {
179
329
  if (!mt5.running) return notConnected();
180
330
  const data = await bcall('positions', {}, 8000);
@@ -194,29 +344,119 @@ const handlers = {
194
344
  days = Math.min(days, 90);
195
345
  const data = await bcall('deals', { days }, 12000);
196
346
  const deals = (data && data.deals) || [];
197
- /* hızlı özet: giriş/çıkış eşleşmesi yapmadan net realized P/L */
198
- let profit = 0;
199
- let wins = 0;
200
- let losses = 0;
201
- for (const dl of deals) {
202
- const p = Number(dl.profit) || 0;
203
- if (String(dl.entry) === '1' || dl.entry === 1 || p !== 0) {
204
- profit += p + (Number(dl.swap) || 0) + (Number(dl.commission) || 0);
205
- if (p > 0) wins++;
206
- else if (p < 0) losses++;
207
- }
208
- }
347
+ const stats = finstats.summarizeDeals(deals);
209
348
  return {
210
349
  ok: true,
211
350
  days,
212
351
  count: deals.length,
213
- netProfit: Math.round(profit * 100) / 100,
214
- wins,
215
- losses,
352
+ netProfit: stats.netProfit,
353
+ wins: stats.wins,
354
+ losses: stats.losses,
355
+ winRate: stats.winRate,
356
+ profitFactor: stats.profitFactor,
357
+ bySymbol: stats.bySymbol,
216
358
  deals: deals.slice(-60),
217
359
  };
218
360
  },
219
- async mt5_trade(args) {
361
+ async mt5_alerts(args) {
362
+ const action = String(args.action || 'list').toLowerCase();
363
+ if (action === 'list') {
364
+ const list = alertsApi.list();
365
+ return { ok: true, count: list.length, alerts: list };
366
+ }
367
+ if (action === 'remove') {
368
+ const id = String(args.id || '').trim();
369
+ if (!id) return { ok: false, error: 'id gerekli' };
370
+ const removed = alertsApi.remove(id);
371
+ return { ok: !!removed, removed: !!removed, id };
372
+ }
373
+ if (action === 'set') {
374
+ const symbol = String(args.symbol || '').trim().toUpperCase();
375
+ const price = Number(args.price);
376
+ const direction = String(args.direction || 'above').toLowerCase();
377
+ if (!symbol) return { ok: false, error: 'symbol gerekli' };
378
+ if (!isFinite(price) || price <= 0) return { ok: false, error: 'price gerekli (pozitif sayı)' };
379
+ if (direction !== 'above' && direction !== 'below') return { ok: false, error: "direction 'above' veya 'below' olmalı" };
380
+ const alarm = alertsApi.set({ symbol, price, direction, note: String(args.note || '').slice(0, 200) });
381
+ if (!alarm) return { ok: false, error: 'alarm kurulamadı' };
382
+ noteTrade('alert-set', { symbol, price, direction, id: alarm.id, note: alarm.note }, {});
383
+ return { ok: true, alert: alarm };
384
+ }
385
+ return { ok: false, error: "action: list|set|remove" };
386
+ },
387
+ async mt5_note(args, ctx) {
388
+ const symbol = String(args.symbol || '').trim().toUpperCase();
389
+ const note = String(args.note || '').trim();
390
+ if (!symbol || !note) return { ok: false, error: 'symbol ve note gerekli' };
391
+ noteTrade('note', {
392
+ symbol,
393
+ note: note.slice(0, 2000),
394
+ ticket: Number(args.ticket) || 0,
395
+ side: String(args.side || '').toLowerCase(),
396
+ }, ctx || {});
397
+ return { ok: true, saved: true };
398
+ },
399
+ async mt5_risksize(args) {
400
+ if (!mt5.running) return notConnected();
401
+ const cfg = getCfg();
402
+ const symbol = String(args.symbol || '').trim().toUpperCase();
403
+ if (!symbol) return { ok: false, error: 'symbol gerekli' };
404
+ const info = await symInfoRow(symbol);
405
+ if (!info) return { ok: false, error: 'sembol bulunamadı: ' + symbol + ' (MT5 Market Watch?)' };
406
+ const side = String(args.side || 'buy').toLowerCase() === 'sell' ? 'sell' : 'buy';
407
+ const entry = Number(args.entry) || tickPrice(info, side);
408
+ let sl = Number(args.sl) || 0;
409
+ let atr = null;
410
+ let timeframe = '';
411
+ if (!(sl > 0) && Number(args.atrMult) > 0) {
412
+ timeframe = String(args.timeframe || 'H1').trim().toUpperCase();
413
+ const rd = await bcall('rates', { symbol, timeframe, count: 100 }, 15000);
414
+ const rates = (rd && rd.rates) || [];
415
+ if (rates.length >= 20) {
416
+ const series = finindicators.atrSeries(
417
+ rates.map((r) => r.high),
418
+ rates.map((r) => r.low),
419
+ rates.map((r) => r.close),
420
+ 14
421
+ );
422
+ atr = series[series.length - 1];
423
+ if (atr > 0) {
424
+ const d = atr * Number(args.atrMult);
425
+ sl = side === 'buy' ? entry - d : entry + d;
426
+ }
427
+ }
428
+ }
429
+ if (!(sl > 0)) return { ok: false, error: 'sl ya da atrMult gerekli' };
430
+ const riskPct = Number(args.riskPct) > 0 ? Number(args.riskPct) : Number(cfg.riskPerTradePct) || 0;
431
+ if (!(riskPct > 0)) return { ok: false, error: 'riskPct gerekli (ayarlarda işlem riski %0)' };
432
+ const acct = await bcall('account', {}, 8000);
433
+ const balance = Number(acct && acct.account && acct.account.balance) || 0;
434
+ if (!(balance > 0)) return { ok: false, error: 'bakiye okunamadı' };
435
+ const riskAmount = (balance * riskPct) / 100;
436
+ const res = finrisk.calcRiskLot(info, entry, sl, riskAmount, cfg.maxLot);
437
+ if (res.error) return { ok: false, error: res.error, raw: res.raw, lossPerLot: res.lossPerLot, riskAmount: Math.round(riskAmount * 100) / 100 };
438
+ const digits = Number(info.digits) || 5;
439
+ const dist = Math.abs(entry - sl);
440
+ const closeErr = finrisk.stopsTooClose(info, entry, sl, 0);
441
+ return {
442
+ ok: true,
443
+ symbol,
444
+ side,
445
+ entry: Number(entry.toFixed(digits)),
446
+ sl: Number(sl.toFixed(digits)),
447
+ volume: res.volume,
448
+ riskPct,
449
+ riskAmount: Math.round(riskAmount * 100) / 100,
450
+ lossPerLot: res.lossPerLot,
451
+ distance: Number(dist.toFixed(digits)),
452
+ atr: atr != null ? Math.round(atr * 1e6) / 1e6 : null,
453
+ timeframe: timeframe || null,
454
+ capped: !!res.capped,
455
+ warning: closeErr || undefined,
456
+ maxLot: Number(cfg.maxLot) || 0.1,
457
+ };
458
+ },
459
+ async mt5_trade(args, ctx) {
220
460
  const cfg = getCfg();
221
461
  if (!cfg.allowTrading) {
222
462
  return { ok: false, error: 'Otomatik işlem KAPALI — analiz/öneri modundasın. İşlem önerisini yaz, açma.' };
@@ -227,27 +467,54 @@ const handlers = {
227
467
  const symbol = String(args.symbol || '').trim().toUpperCase();
228
468
  if (!symbol) return { ok: false, error: 'symbol gerekli' };
229
469
  const maxLot = Number(cfg.maxLot) || 0.1;
230
- const vol = clampLot(args.volume, maxLot);
231
- if (!vol) return { ok: false, error: 'volume gerekli (pozitif sayı)' };
470
+ const sl = Number(args.sl) || 0;
471
+ const tp = Number(args.tp) || 0;
472
+ const info = await symInfoRow(symbol);
473
+ if (!info) return { ok: false, error: 'sembol bulunamadı: ' + symbol + ' (MT5 Market Watch?)' };
474
+ const price = tickPrice(info, side);
475
+ /* LOT: riskPct verilirse (ya da volume yokken ayar riski açıksa) SL
476
+ mesafesinden hesaplanır; aksi halde volume broker kuralına normalize edilir */
477
+ const explicitRisk = Number(args.riskPct) > 0 ? Number(args.riskPct) : 0;
478
+ const wantVolume = Number(args.volume);
479
+ let vol = 0;
480
+ let riskInfo = null;
481
+ if (explicitRisk > 0 || (!(wantVolume > 0) && Number(cfg.riskPerTradePct) > 0)) {
482
+ if (!(sl > 0)) return { ok: false, error: '%risk ile lot için sl zorunlu — sl ver ya da volume kullan' };
483
+ const riskPct = explicitRisk > 0 ? explicitRisk : Number(cfg.riskPerTradePct);
484
+ const acct = await bcall('account', {}, 8000);
485
+ const balance = Number(acct && acct.account && acct.account.balance) || 0;
486
+ const riskAmount = (balance * riskPct) / 100;
487
+ const rr = finrisk.calcRiskLot(info, price, sl, riskAmount, maxLot);
488
+ if (rr.error) return { ok: false, error: rr.error };
489
+ vol = rr.volume;
490
+ riskInfo = { riskPct, riskAmount: Math.round(riskAmount * 100) / 100, lossPerLot: rr.lossPerLot };
491
+ } else {
492
+ const norm = finrisk.normalizeVolume(info, wantVolume, maxLot);
493
+ if (norm.error) return { ok: false, error: norm.error };
494
+ vol = norm.volume;
495
+ }
232
496
  const pos = await bcall('positions', {}, 8000);
497
+ const list = (pos && pos.positions) || [];
233
498
  const maxPos = Number(cfg.maxPositions) || 3;
234
- const openCount = ((pos && pos.positions) || []).length;
235
- if (openCount >= maxPos) {
236
- return { ok: false, error: `max eşzamanlı pozisyon dolu (${openCount}/${maxPos}) — önce bir pozisyon kapat` };
499
+ if (list.length >= maxPos) {
500
+ return { ok: false, error: `max eşzamanlı pozisyon dolu (${list.length}/${maxPos}) — önce bir pozisyon kapat` };
237
501
  }
502
+ /* risk katmanı: stops_level + yoğunluk/net yön + marj kalkanı */
503
+ const riskErr = await preTradeCheck(cfg, side, symbol, info, price, vol, sl, tp, list);
504
+ if (riskErr) return { ok: false, error: riskErr };
238
505
  const data = await bcall('market', {
239
506
  symbol,
240
507
  side,
241
508
  volume: vol,
242
- sl: Number(args.sl) || 0,
243
- tp: Number(args.tp) || 0,
509
+ sl,
510
+ tp,
244
511
  deviation: 20,
245
512
  comment: String(args.comment || 'Beast').slice(0, 26),
246
513
  }, 20000);
247
- noteTrade('trade', { symbol, side, volume: vol, result: data && data.result });
248
- return { ok: true, opened: { symbol, side, volume: vol, sl: Number(args.sl) || 0, tp: Number(args.tp) || 0 }, result: data && data.result };
514
+ noteTrade('trade', { symbol, side, volume: vol, sl, tp, risk: riskInfo, comment: String(args.comment || ''), result: data && data.result }, ctx);
515
+ return { ok: true, opened: { symbol, side, volume: vol, sl, tp }, risk: riskInfo, result: data && data.result };
249
516
  },
250
- async mt5_close(args) {
517
+ async mt5_close(args, ctx) {
251
518
  const cfg = getCfg();
252
519
  if (!mt5.running) return notConnected();
253
520
  const ticket = Number(args.ticket);
@@ -258,10 +525,10 @@ const handlers = {
258
525
  if (vol > maxLot) return { ok: false, error: `kısmi kapatma lotu limiti aşıyor (max ${maxLot})` };
259
526
  }
260
527
  const data = await bcall('close', { ticket, volume: vol || 0 }, 20000);
261
- noteTrade('close', { ticket, volume: vol || 'all', result: data && data.result });
528
+ noteTrade('close', { ticket, volume: vol || 'all', result: data && data.result }, ctx);
262
529
  return { ok: true, result: data && data.result };
263
530
  },
264
- async mt5_modify(args) {
531
+ async mt5_modify(args, ctx) {
265
532
  if (!mt5.running) return notConnected();
266
533
  const ticket = Number(args.ticket);
267
534
  if (!ticket) return { ok: false, error: 'ticket gerekli' };
@@ -270,20 +537,24 @@ const handlers = {
270
537
  sl: Number(args.sl) || 0,
271
538
  tp: Number(args.tp) || 0,
272
539
  }, 20000);
273
- noteTrade('modify', { ticket, sl: Number(args.sl) || 0, tp: Number(args.tp) || 0 });
540
+ noteTrade('modify', { ticket, sl: Number(args.sl) || 0, tp: Number(args.tp) || 0 }, ctx);
274
541
  return { ok: true, result: data && data.result };
275
542
  },
276
- async mt5_pending(args) {
543
+ async mt5_pending(args, ctx) {
277
544
  const cfg = getCfg();
278
545
  if (!cfg.allowTrading) {
279
546
  return { ok: false, error: 'Otomatik işlem KAPALI — bekleyen emir de açılamaz. Öneriyi yaz.' };
280
547
  }
281
548
  if (!mt5.running) return notConnected();
282
- const maxLot = Number(cfg.maxLot) || 0.1;
283
- const vol = clampLot(args.volume, maxLot);
284
- if (!vol) return { ok: false, error: 'volume gerekli' };
549
+ const symbol = String(args.symbol || '').trim().toUpperCase();
550
+ if (!symbol) return { ok: false, error: 'symbol gerekli' };
551
+ const info = await symInfoRow(symbol);
552
+ if (!info) return { ok: false, error: 'sembol bulunamadı: ' + symbol + ' (MT5 Market Watch?)' };
553
+ const norm = finrisk.normalizeVolume(info, args.volume, Number(cfg.maxLot) || 0.1);
554
+ if (norm.error) return { ok: false, error: norm.error };
555
+ const vol = norm.volume;
285
556
  const data = await bcall('pending', {
286
- symbol: String(args.symbol || '').trim().toUpperCase(),
557
+ symbol,
287
558
  type: String(args.type || '').toLowerCase(),
288
559
  volume: vol,
289
560
  price: Number(args.price) || 0,
@@ -291,17 +562,17 @@ const handlers = {
291
562
  tp: Number(args.tp) || 0,
292
563
  comment: String(args.comment || 'Beast').slice(0, 26),
293
564
  }, 20000);
294
- noteTrade('pending', { symbol: args.symbol, type: args.type, volume: vol });
565
+ noteTrade('pending', { symbol, type: args.type, volume: vol }, ctx);
295
566
  return { ok: true, result: data && data.result };
296
567
  },
297
- async mt5_cancel(args) {
568
+ async mt5_cancel(args, ctx) {
298
569
  if (!mt5.running) return notConnected();
299
570
  const ticket = Number(args.ticket);
300
571
  if (!ticket) return { ok: false, error: 'ticket gerekli' };
301
572
  const data = await bcall('cancel', { ticket }, 20000);
302
- noteTrade('cancel', { ticket });
573
+ noteTrade('cancel', { ticket }, ctx);
303
574
  return { ok: true, result: data && data.result };
304
575
  },
305
576
  };
306
577
 
307
- module.exports = { definitions, handlers, NAMES, setConfig, setNotify };
578
+ module.exports = { definitions, handlers, NAMES, setConfig, setNotify, setAlerts };