beast-agent 2.38.2 → 2.38.3

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.38.2",
4
+ "version": "2.38.3",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -1920,24 +1920,29 @@ class Engine {
1920
1920
  '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.',
1921
1921
  })[String((session && session.financeRole) || '')] || '';
1922
1922
  /* ROL → SKILL eşleştirmesi (ayarlar modalı): rol başına TEK ve ZORUNLU
1923
- skill. Seçili SKILL.md'nin TAM GÖVDESİ prompta gömülür — ajan skill
1924
- aracını çağırmayı atlasa bile prosedür KESİN uygulanır. Eşleştirme
1925
- boşsa satır eklenmez; ajan skill'i göreve göre kendi seçer (SKILLS). */
1923
+ skill; ANA TRADER için ayrıca PLAYBOOK skill'i (roleSkills.trader).
1924
+ Seçili SKILL.md'nin TAM GÖVDESİ prompta gömülür — ajan skill aracını
1925
+ çağırmayı atlasa bile prosedür KESİN uygulanır. */
1926
1926
  const roleSkills = Array.isArray(session && session.financeRoleSkills)
1927
1927
  ? session.financeRoleSkills.map((s) => String(s || '').trim()).filter(Boolean).slice(0, 1)
1928
1928
  : [];
1929
1929
  let roleSkillBlock = '';
1930
- if (roleBlock && roleSkills.length) {
1930
+ if (roleSkills.length) {
1931
1931
  const roleSkillName = roleSkills[0];
1932
1932
  const sb = this._skillBody({ name: roleSkillName });
1933
+ const what = roleBlock
1934
+ ? 'Bu rolün ZORUNLU skill\'i'
1935
+ : session && session.financePlaybook
1936
+ ? 'TRADER PLAYBOOK\'un (ZORUNLU skill)'
1937
+ : 'BU OTURUMUN ZORUNLU skill\'i';
1933
1938
  roleSkillBlock =
1934
- ` Bu rolün ZORUNLU skill'i: skill("${roleSkillName}")` +
1939
+ ` ${what}: skill("${roleSkillName}")` +
1935
1940
  (sb && sb.ok && sb.content
1936
1941
  ? ' — aşağıda TAM prosedürü verilmiştir; HER TURDA birebir uygula (ayrıca okumana gerek yok):\n\n' +
1937
1942
  `===== SKILL: ${roleSkillName} =====\n${sb.content}\n===== /SKILL =====`
1938
1943
  : ' — ilk turda bu skill\'i mutlaka oku ve prosedürüne birebir uy.');
1939
1944
  }
1940
- const roleBlockFull = roleBlock ? roleBlock + roleSkillBlock : '';
1945
+ const roleBlockFull = roleBlock + roleSkillBlock;
1941
1946
  /* FİNANS EKİBİ: tüm finance ajanları tek DM grubundadır — sohbet grup
1942
1947
  thread'inde toplanır (ayrı ayrı 1:1 thread'lere dağılmaz) */
1943
1948
  const finJob = this._bgJobs && this._bgJobs.get(String(session && session.id));
@@ -1967,6 +1972,8 @@ class Engine {
1967
1972
  '- Emin olmadığında İŞLEM YOK — "BEKLE: <sebep>" yaz. Sık işlem > iyi işlem.\n' +
1968
1973
  '- Hesap kaldıracı ve serbest marja göre pozisyon boyutunu düşük tut; tek işlemde serbest marjın büyük kısmını riske atma.\n' +
1969
1974
  (session && session.financeStrategy ? `SAHİBİNİN STRATEJİ NOTU (önceliklidir):\n${session.financeStrategy}\n` : '') +
1975
+ (session && session.financeShadow ? 'SHADOW MOD AKTİF: mt5_trade/mt5_pending emir GÖNDERMEZ — kararını teziyle raporla; gerçek işlem açılmaz (karar günlüğe yazılır).\n' : '') +
1976
+ (session && session.financeDigest ? `İŞLEM PERFORMANS GEÇMİŞİN (kendi kayıtların — kararlarında ders çıkar):\n${session.financeDigest}\n` : '') +
1970
1977
  (mem.user ? `# USER\n${mem.user}\n` : '') +
1971
1978
  (rules.length ? `# KURALLAR (sahibinin kalıcı talimatları — daima uy)\n${rules.map((r) => '- ' + r).join('\n')}\n` : '') +
1972
1979
  (skList.length
@@ -31,6 +31,10 @@ const NAMES = [
31
31
 
32
32
  let getCfg = () => ({ allowTrading: false, maxLot: 0.1, maxPositions: 3, symbols: [] });
33
33
  let notify = () => {};
34
+ /* DİSİPLİN KANCASI (main enjekte eder): kodla zorlanan kurallar — günlük işlem
35
+ limiti, kayıp serisi molası, re-entry beklemesi, kur maruziyeti.
36
+ (side, symbol, positions) → hata metni ya da null. */
37
+ let discipline = () => null;
34
38
  /* alarm deposu main süreçte yaşar (dosyaya kalıcı) — buradan enjekte edilir */
35
39
  let alertsApi = {
36
40
  list: () => [],
@@ -50,6 +54,10 @@ function setAlerts(api) {
50
54
  if (api && typeof api === 'object') alertsApi = api;
51
55
  }
52
56
 
57
+ function setDiscipline(fn) {
58
+ if (typeof fn === 'function') discipline = fn;
59
+ }
60
+
53
61
  async function bcall(method, params, timeoutMs) {
54
62
  const r = await mt5.call(method, params, timeoutMs);
55
63
  if (!r || r.ok !== true) throw new Error((r && r.error) || 'MT5 hatası');
@@ -75,6 +83,10 @@ async function preTradeCheck(cfg, side, symbol, info, price, volume, sl, tp, pos
75
83
  if (closeErr) return closeErr;
76
84
  const expErr = finrisk.exposureCheck(positions || [], side, symbol, cfg.maxPerSymbol, cfg.maxSameSide);
77
85
  if (expErr) return expErr;
86
+ /* KODLA DİSİPLİN: günlük limit / kayıp serisi molası / re-entry / kur maruziyeti */
87
+ let discErr = null;
88
+ try { discErr = discipline(side, symbol, positions || []); } catch {}
89
+ if (discErr) return discErr;
78
90
  const minLevel = Number(cfg.minMarginLevel) || 0;
79
91
  let account = null;
80
92
  let estMargin = 0;
@@ -216,7 +228,7 @@ const definitions = NAMES.map((name) => {
216
228
  },
217
229
  mt5_trade: {
218
230
  description:
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.',
231
+ 'PİYASA EMRİ AÇAR: mt5_trade {symbol, side:"buy"|"sell", volume, sl?, tp?, comment?, reason?}. Lot limiti ve max pozisyon sayısı sistem tarafından zorlanır. Otomatik işlem anahtarı kapalıysa reddedilir. SL/TP vermek ŞIDDETLİ önerilir. reason: kararın tek cümlelik tezi (günlüğe yazılır, performans değerlendirmesinde kullanılır).',
220
232
  parameters: {
221
233
  type: 'object',
222
234
  properties: {
@@ -226,6 +238,7 @@ const definitions = NAMES.map((name) => {
226
238
  sl: { type: 'number', description: 'Stop loss fiyatı (0 = yok)' },
227
239
  tp: { type: 'number', description: 'Take profit fiyatı (0 = yok)' },
228
240
  comment: { type: 'string', description: 'Kısa işlem notu' },
241
+ reason: { type: 'string', description: 'Kararın tezi/gerekçesi (tek cümle)' },
229
242
  },
230
243
  required: ['symbol', 'side', 'volume'],
231
244
  },
@@ -255,7 +268,7 @@ const definitions = NAMES.map((name) => {
255
268
  },
256
269
  mt5_pending: {
257
270
  description:
258
- 'BEKLEYEN EMİR koyar: mt5_pending {symbol, type:"buy_limit"|"sell_limit"|"buy_stop"|"sell_stop", volume, price, sl?, tp?}. Otomatik işlem anahtarına tabidir.',
271
+ 'BEKLEYEN EMİR koyar: mt5_pending {symbol, type:"buy_limit"|"sell_limit"|"buy_stop"|"sell_stop", volume, price, sl?, tp?, reason?}. Otomatik işlem anahtarına tabidir. reason: kararın tezi (günlüğe yazılır).',
259
272
  parameters: {
260
273
  type: 'object',
261
274
  properties: {
@@ -265,6 +278,7 @@ const definitions = NAMES.map((name) => {
265
278
  price: { type: 'number' },
266
279
  sl: { type: 'number' },
267
280
  tp: { type: 'number' },
281
+ reason: { type: 'string', description: 'Kararın tezi/gerekçesi (tek cümle)' },
268
282
  },
269
283
  required: ['symbol', 'type', 'volume', 'price'],
270
284
  },
@@ -499,9 +513,28 @@ const handlers = {
499
513
  if (list.length >= maxPos) {
500
514
  return { ok: false, error: `max eşzamanlı pozisyon dolu (${list.length}/${maxPos}) — önce bir pozisyon kapat` };
501
515
  }
502
- /* risk katmanı: stops_level + yoğunluk/net yön + marj kalkanı */
516
+ /* risk katmanı: stops_level + yoğunluk/net yön + marj kalkanı + disiplin */
503
517
  const riskErr = await preTradeCheck(cfg, side, symbol, info, price, vol, sl, tp, list);
504
518
  if (riskErr) return { ok: false, error: riskErr };
519
+ /* SHADOW MOD: gerçek emir GÖNDERİLMEZ — karar teziyle günlüğe yazılır */
520
+ if (cfg.shadowMode) {
521
+ noteTrade('shadow', {
522
+ symbol,
523
+ side,
524
+ volume: vol,
525
+ sl,
526
+ tp,
527
+ reason: String(args.reason || '').slice(0, 500),
528
+ risk: riskInfo,
529
+ }, ctx);
530
+ return {
531
+ ok: true,
532
+ shadow: true,
533
+ planned: { symbol, side, volume: vol, sl, tp },
534
+ risk: riskInfo,
535
+ note: 'SHADOW MOD: emir GÖNDERİLMEDİ, karar günlüğe yazıldı. Gerçek işlem için TRADE AJANI ayarlarından shadow modu kapat.',
536
+ };
537
+ }
505
538
  const data = await bcall('market', {
506
539
  symbol,
507
540
  side,
@@ -511,7 +544,7 @@ const handlers = {
511
544
  deviation: 20,
512
545
  comment: String(args.comment || 'Beast').slice(0, 26),
513
546
  }, 20000);
514
- noteTrade('trade', { symbol, side, volume: vol, sl, tp, risk: riskInfo, comment: String(args.comment || ''), result: data && data.result }, ctx);
547
+ noteTrade('trade', { symbol, side, volume: vol, sl, tp, risk: riskInfo, comment: String(args.comment || ''), reason: String(args.reason || '').slice(0, 500), result: data && data.result }, ctx);
515
548
  return { ok: true, opened: { symbol, side, volume: vol, sl, tp }, risk: riskInfo, result: data && data.result };
516
549
  },
517
550
  async mt5_close(args, ctx) {
@@ -553,16 +586,27 @@ const handlers = {
553
586
  const norm = finrisk.normalizeVolume(info, args.volume, Number(cfg.maxLot) || 0.1);
554
587
  if (norm.error) return { ok: false, error: norm.error };
555
588
  const vol = norm.volume;
589
+ const ptype = String(args.type || '').toLowerCase();
590
+ const pside = ptype.startsWith('buy') ? 'buy' : ptype.startsWith('sell') ? 'sell' : '';
591
+ /* KODLA DİSİPLİN: bekleyen emir de işlem sayılır */
592
+ let discErr = null;
593
+ try { discErr = discipline(pside, symbol, []); } catch {}
594
+ if (discErr) return { ok: false, error: discErr };
595
+ /* SHADOW MOD: bekleyen emir de GÖNDERİLMEZ — tez günlüğe düşer */
596
+ if (cfg.shadowMode) {
597
+ noteTrade('shadow', { symbol, type: ptype, side: pside, volume: vol, price: Number(args.price) || 0, sl: Number(args.sl) || 0, tp: Number(args.tp) || 0, reason: String(args.reason || '').slice(0, 500) }, ctx);
598
+ return { ok: true, shadow: true, note: 'SHADOW MOD: bekleyen emir GÖNDERİLMEDİ, karar günlüğe yazıldı.' };
599
+ }
556
600
  const data = await bcall('pending', {
557
601
  symbol,
558
- type: String(args.type || '').toLowerCase(),
602
+ type: ptype,
559
603
  volume: vol,
560
604
  price: Number(args.price) || 0,
561
605
  sl: Number(args.sl) || 0,
562
606
  tp: Number(args.tp) || 0,
563
607
  comment: String(args.comment || 'Beast').slice(0, 26),
564
608
  }, 20000);
565
- noteTrade('pending', { symbol, type: args.type, volume: vol }, ctx);
609
+ noteTrade('pending', { symbol, type: args.type, volume: vol, reason: String(args.reason || '').slice(0, 500) }, ctx);
566
610
  return { ok: true, result: data && data.result };
567
611
  },
568
612
  async mt5_cancel(args, ctx) {
@@ -575,4 +619,4 @@ const handlers = {
575
619
  },
576
620
  };
577
621
 
578
- module.exports = { definitions, handlers, NAMES, setConfig, setNotify, setAlerts };
622
+ module.exports = { definitions, handlers, NAMES, setConfig, setNotify, setAlerts, setDiscipline };
@@ -129,4 +129,113 @@ function exposureCheck(positions, side, symbol, maxPerSymbol, maxSameSide) {
129
129
  return null;
130
130
  }
131
131
 
132
- module.exports = { normalizeVolume, lossPerLot, calcRiskLot, stopsTooClose, marginCheck, exposureCheck };
132
+ /* ---------- DİSİPLİN (kodla zorlanan trader kuralları) ----------
133
+ Prompt ricalarına güvenmeden uygulanır: günlük işlem limiti, ardışık kayıp
134
+ molası, sembol re-entry beklemesi ve yönlü kur maruziyeti (korelasyon). */
135
+
136
+ const SYM_ALIAS = {
137
+ XAU: 'XAU', GOLD: 'XAU', XAUT: 'XAU',
138
+ XAG: 'XAG', SILV: 'XAG', SILVER: 'XAG',
139
+ WTI: 'OIL', BREN: 'OIL', BRENT: 'OIL', USOI: 'OIL', UKOI: 'OIL',
140
+ NATG: 'GAS', NGAS: 'GAS',
141
+ };
142
+
143
+ /* Sembolün para bacakları: EURUSD → [EUR,USD]; GOLD → [XAU,USD]; bilinmezse [] */
144
+ function symbolLegs(symbol) {
145
+ const s = String(symbol || '').toUpperCase().replace(/[^A-Z]/g, '');
146
+ if (/^[A-Z]{6}$/.test(s)) return [s.slice(0, 3), s.slice(3)];
147
+ const base = SYM_ALIAS[s.slice(0, 4)] || SYM_ALIAS[s.slice(0, 3)];
148
+ if (base) return [base, 'USD'];
149
+ if (/^USD[A-Z]{3}$/.test(s)) return ['USD', s.slice(3)];
150
+ return [];
151
+ }
152
+
153
+ /* Yönlü kur maruziyeti: buy EURUSD = +EUR −USD. Limitsizse null. */
154
+ function currencyExposureError(positions, side, symbol, maxPerCurrency) {
155
+ const max = num(maxPerCurrency, 0);
156
+ if (!(max > 0)) return null;
157
+ const legs = symbolLegs(symbol);
158
+ if (!legs.length) return null;
159
+ const dir = String(side).toLowerCase() === 'buy' ? 1 : -1;
160
+ const exp = {};
161
+ const add = (cur, d) => { if (cur) exp[cur] = (exp[cur] || 0) + d; };
162
+ for (const p of (Array.isArray(positions) ? positions : [])) {
163
+ const l = symbolLegs(p && p.symbol);
164
+ if (!l.length) continue;
165
+ const d = Number(p.type) === 0 ? 1 : -1;
166
+ add(l[0], d);
167
+ add(l[1], -d);
168
+ }
169
+ add(legs[0], dir);
170
+ add(legs[1], -dir);
171
+ for (const [cur, v] of Object.entries(exp)) {
172
+ if (Math.abs(v) > max) {
173
+ return `${cur} yönlü maruziyet ${v > 0 ? '+' : ''}${v} (max ±${max}) — korelasyon riski; farklı parite/yöne geç`;
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+
179
+ /* Zaman/geçmiş tabanlı disiplin. entries: journal kayıtları (at, kind, symbol, net).
180
+ Sorun varsa Türkçe hata metni, yoksa null döner. Limit 0 = kural kapalı. */
181
+ function disciplineError(entries, now, cfg, side, symbol, positions) {
182
+ const t = num(now, Date.now());
183
+ /* gelecekteki kayıtlar (saat kayması) değerlendirmeye girmez */
184
+ const list = (Array.isArray(entries) ? entries : []).filter((e) => e && Number.isFinite(Number(e.at)) && Number(e.at) <= t);
185
+ const f = cfg || {};
186
+ const dayStart = (() => { const d = new Date(t); d.setHours(0, 0, 0, 0); return d.getTime(); })();
187
+ const maxDay = num(f.maxTradesPerDay, 0);
188
+ if (maxDay > 0) {
189
+ const opens = list.filter((e) => e.kind === 'open' && Number(e.at) >= dayStart).length;
190
+ if (opens >= maxDay) {
191
+ return `günlük işlem limiti dolu (${opens}/${maxDay}) — bugün yeni işlem açma`;
192
+ }
193
+ }
194
+ const streakLimit = num(f.lossStreakLimit, 0);
195
+ const pauseMin = num(f.lossStreakPauseMin, 0);
196
+ if (streakLimit > 0 && pauseMin > 0) {
197
+ const closes = list.filter((e) => e.kind === 'close').slice().sort((a, b) => num(b.at, 0) - num(a.at, 0));
198
+ let streak = 0;
199
+ let lastAt = 0;
200
+ for (const c of closes) {
201
+ if (num(c.net, 0) < 0) {
202
+ streak++;
203
+ if (!lastAt) lastAt = num(c.at, 0);
204
+ } else break;
205
+ }
206
+ if (streak >= streakLimit && lastAt) {
207
+ const elapsed = t - lastAt;
208
+ if (elapsed < pauseMin * 60000) {
209
+ const rem = Math.ceil((pauseMin * 60000 - elapsed) / 60000);
210
+ return `${streak} ardışık kayıp — seri molası ${rem} dk daha (limit ${streakLimit} kayıp → ${pauseMin} dk mola)`;
211
+ }
212
+ }
213
+ }
214
+ const reMin = num(f.reentryCooldownMin, 0);
215
+ if (reMin > 0) {
216
+ const sym = String(symbol || '').toUpperCase();
217
+ const last = list
218
+ .filter((e) => e.kind === 'close' && String(e.symbol || '').toUpperCase() === sym)
219
+ .sort((a, b) => num(b.at, 0) - num(a.at, 0))[0];
220
+ if (last) {
221
+ const elapsed = t - num(last.at, 0);
222
+ if (elapsed < reMin * 60000) {
223
+ const rem = Math.ceil((reMin * 60000 - elapsed) / 60000);
224
+ return `${sym} son işlemi ${Math.max(1, Math.round(elapsed / 60000))} dk önce kapandı — re-entry için ${rem} dk bekle`;
225
+ }
226
+ }
227
+ }
228
+ return currencyExposureError(positions, side, symbol, f.maxPerCurrency);
229
+ }
230
+
231
+ module.exports = {
232
+ normalizeVolume,
233
+ lossPerLot,
234
+ calcRiskLot,
235
+ stopsTooClose,
236
+ marginCheck,
237
+ exposureCheck,
238
+ symbolLegs,
239
+ currencyExposureError,
240
+ disciplineError,
241
+ };
@@ -189,6 +189,16 @@ function buildWeeklyReport(ctx) {
189
189
  for (const [d, v] of days) L.push('- ' + d + ': ' + money(v, cur));
190
190
  L.push('');
191
191
  }
192
+ if (c.mfe && c.mfe.n) {
193
+ L.push('## Kâr Yakalama (MFE/MAE)');
194
+ if (c.mfe.capture != null) {
195
+ L.push('- Kazanan işlemlerde ulaşılan maksimum kârın **%' + c.mfe.capture + '**\'i realize edildi (' + c.mfe.n + ' kapanış ölçüldü).');
196
+ }
197
+ if (c.mfe.avgMaeLoss != null) {
198
+ L.push('- Kaybedenlerde ortalama MAE (en kötü seviye): **-' + c.mfe.avgMaeLoss + '** — SL mesafesi bu veriye göre ayarlanabilir.');
199
+ }
200
+ L.push('');
201
+ }
192
202
  const syms = Object.entries(st.bySymbol || {})
193
203
  .map(([s, v]) => ({ symbol: s, ...v }))
194
204
  .sort((a, b) => b.net - a.net)
@@ -43,10 +43,13 @@ def d(obj):
43
43
 
44
44
  TERMINAL_PATH = ""
45
45
  INIT_TRIED = False
46
+ ALLOW_LAUNCH = True # Beast Finance kapaliyken False: MT5 kapaliysa ACILMAZ
46
47
 
47
48
 
48
49
  def ensure_init():
49
- """Terminal baglantisi yoksa (yeniden) init dener; True/False doner."""
50
+ """Terminal baglantisi yoksa (yeniden) init dener; True/False doner.
51
+ ALLOW_LAUNCH False iken terminal kapaliysa BASLATMA denenmez — Beast
52
+ Finance acilmadikca MT5 kendiliginden acilmaz."""
50
53
  global INIT_TRIED
51
54
  if mt5 is None:
52
55
  return False
@@ -55,6 +58,8 @@ def ensure_init():
55
58
  return True
56
59
  except Exception:
57
60
  pass
61
+ if not ALLOW_LAUNCH:
62
+ return False
58
63
  INIT_TRIED = True
59
64
  if TERMINAL_PATH:
60
65
  return bool(mt5.initialize(path=TERMINAL_PATH))
@@ -65,6 +70,8 @@ def need():
65
70
  if mt5 is None:
66
71
  raise RuntimeError("MetaTrader5 python paketi kurulu degil (pip install MetaTrader5)")
67
72
  if not ensure_init():
73
+ if not ALLOW_LAUNCH:
74
+ raise RuntimeError("MT5 terminali kapali — Beast Finance acilinca otomatik baslatilir")
68
75
  last = ""
69
76
  try:
70
77
  last = str(mt5.last_error())
@@ -76,7 +83,16 @@ def need():
76
83
  # ---------------- yontemler ----------------
77
84
 
78
85
  def h_ping(p):
79
- return {"pong": True, "mt5": mt5 is not None, "connected": bool(ensure_init()) if mt5 else False}
86
+ return {"pong": True, "mt5": mt5 is not None, "connected": bool(ensure_init()) if mt5 else False, "launch": ALLOW_LAUNCH}
87
+
88
+
89
+ def h_policy(p):
90
+ """Otomatik baslatma politikasi: {"launch": bool}. Beast Finance
91
+ kapatilinca launch=False gonderilir — MT5 kapaliysa bir daha ACILMAZ;
92
+ terminal aciksa cagrilar calismaya devam eder (koruma surer)."""
93
+ global ALLOW_LAUNCH
94
+ ALLOW_LAUNCH = p.get("launch") is not False
95
+ return {"launch": ALLOW_LAUNCH}
80
96
 
81
97
 
82
98
  def h_account(p):
@@ -375,6 +391,7 @@ def h_all_symbols(p):
375
391
 
376
392
  HANDLERS = {
377
393
  "ping": h_ping,
394
+ "policy": h_policy,
378
395
  "account": h_account,
379
396
  "terminal": h_terminal,
380
397
  "symbols": h_symbols,
package/src/main.js CHANGED
@@ -6904,6 +6904,12 @@ function cronMirrorTargets(cronSid) {
6904
6904
  }
6905
6905
 
6906
6906
  function cronFire(job) {
6907
+ /* BEAST FINANCE günlük rutin: plan/review job'ları trader'a özel gönderilir */
6908
+ if (job && (job.kind === 'finance-plan' || job.kind === 'finance-review')) {
6909
+ try { finDailyRoutine(job.kind === 'finance-plan' ? 'plan' : 'review'); } catch {}
6910
+ cronEmit();
6911
+ return;
6912
+ }
6907
6913
  try {
6908
6914
  const sid = reuseOrLatestSession(job.sessionId);
6909
6915
  if (sid !== String(job.sessionId || '')) {
@@ -8431,6 +8437,7 @@ function studioWatchStop() {
8431
8437
  oturumu, tur tur piyasa tarayıp (farklı API/model seçilebilir) işlem kovalar. */
8432
8438
  const mt5bridge = require('./mt5bridge');
8433
8439
  const financetools = require('./agent/financetools');
8440
+ const finrisk = require('./agent/finrisk');
8434
8441
  const customtools = require('./agent/customtools');
8435
8442
  const finwatch = require('./agent/finwatch');
8436
8443
  const finstats = require('./agent/finstats');
@@ -8445,6 +8452,9 @@ const financeState = {
8445
8452
  installTried: false,
8446
8453
  agents: new Map(), /* sid -> { symbols, main, round, timer } — AYNI ANDA koşan finance ajanları */
8447
8454
  watch: new Map(), /* ticket -> risk otomasyonu durumu (R, BE, kısmi TP...) */
8455
+ excursions: new Map(), /* ticket -> { symbol, side, mfe, mae, at } — MAE/MFE takibi */
8456
+ excDirty: false,
8457
+ excSavedAt: 0,
8448
8458
  watchTimer: null,
8449
8459
  watchBusy: false,
8450
8460
  watchTickAt: 0,
@@ -8495,9 +8505,25 @@ function finCfg() {
8495
8505
  const cur = Array.isArray(f.roleSkills[d.id]) ? f.roleSkills[d.id] : [];
8496
8506
  f.roleSkills[d.id] = cur.map((s) => String(s || '').trim()).filter(Boolean).slice(0, 1);
8497
8507
  }
8508
+ /* TRADER PLAYBOOK: asıl karar vericinin zorunlu skill'i (roleSkills.trader) */
8509
+ {
8510
+ const cur = Array.isArray(f.roleSkills.trader) ? f.roleSkills.trader : [];
8511
+ f.roleSkills.trader = cur.map((s) => String(s || '').trim()).filter(Boolean).slice(0, 1);
8512
+ }
8498
8513
  if (!Number(f.intervalSec)) f.intervalSec = 120;
8499
8514
  if (!Number(f.maxLot)) f.maxLot = 0.1;
8500
8515
  if (f.maxPositions == null) f.maxPositions = 3;
8516
+ /* KODLA DİSİPLİN (trader kuralları): hepsi 0 = kural kapalı */
8517
+ if (!Number.isFinite(Number(f.maxTradesPerDay))) f.maxTradesPerDay = 10;
8518
+ if (!Number.isFinite(Number(f.lossStreakLimit))) f.lossStreakLimit = 2;
8519
+ if (!Number.isFinite(Number(f.lossStreakPauseMin))) f.lossStreakPauseMin = 30;
8520
+ if (!Number.isFinite(Number(f.reentryCooldownMin))) f.reentryCooldownMin = 15;
8521
+ if (!Number.isFinite(Number(f.maxPerCurrency))) f.maxPerCurrency = 3;
8522
+ /* SHADOW MOD: emir gönderilmez — kararlar gerekçesiyle günlüğe yazılır */
8523
+ if (typeof f.shadowMode !== 'boolean') f.shadowMode = false;
8524
+ /* GÜNLÜK RUTİN: plan/review saatleri (HH:MM; boş = kapalı) — hafta içi cron */
8525
+ if (typeof f.planTime !== 'string') f.planTime = '';
8526
+ if (typeof f.reviewTime !== 'string') f.reviewTime = '';
8501
8527
  /* RİSK OTOMASYONU (watchdog): +R'da BE, trailing, kısmi TP — main süreç
8502
8528
  saniyelik döngüyle uygular; ajan turunu BEKLEMEZ. 0 = ilgili kural kapalı. */
8503
8529
  if (typeof f.watchdog !== 'boolean') f.watchdog = true;
@@ -8605,6 +8631,121 @@ function finJournalTail(n) {
8605
8631
  }
8606
8632
  }
8607
8633
 
8634
+ /* ---------- GERİ BİLDİRİM DÖNGÜSÜ: ajanın kendi işlem geçmişi özeti ----------
8635
+ Trader her turda son kapanışlarını, bugününü, kayıp serisini, kâr yakalama
8636
+ oranını (MFE) ve son notlarını sistem promptunda görür. */
8637
+ function finBuildDigest() {
8638
+ let entries = [];
8639
+ try { entries = finJournalTail(300); } catch { return ''; }
8640
+ const closesAll = entries.filter((e) => e.kind === 'close');
8641
+ const closes = closesAll.slice(-10);
8642
+ const L = [];
8643
+ const dayStart = (() => { const d = new Date(); d.setHours(0, 0, 0, 0); return d.getTime(); })();
8644
+ const todayOpens = entries.filter((e) => e.kind === 'open' && Number(e.at) >= dayStart).length;
8645
+ const todayCloses = closesAll.filter((e) => Number(e.at) >= dayStart);
8646
+ const todayNet = Math.round(todayCloses.reduce((a, e) => a + (Number(e.net) || 0), 0) * 100) / 100;
8647
+ const f = finCfg();
8648
+ L.push(`Bugün: ${todayOpens} açılış · ${todayCloses.length} kapanış · net ${todayNet >= 0 ? '+' : ''}${todayNet}`);
8649
+ if (closes.length) {
8650
+ L.push('Son kapanışlar (yeni→eski): ' + closes
8651
+ .slice()
8652
+ .reverse()
8653
+ .map((e) => `${e.symbol || '?'}${e.side ? ' ' + String(e.side).toUpperCase() : ''} ${Number(e.net) >= 0 ? '+' : ''}${Number(e.net) || 0}`)
8654
+ .join(' · '));
8655
+ }
8656
+ let streak = 0;
8657
+ for (const c of closesAll.slice().reverse()) {
8658
+ if ((Number(c.net) || 0) < 0) streak++;
8659
+ else break;
8660
+ }
8661
+ if (streak >= 2) L.push(`DİKKAT: ${streak} ardışık kayıp — seri molası kuralı ${f.lossStreakLimit} kayıpta devreye girer`);
8662
+ /* MFE (maksimum kâr) yakalama oranı: kârı ne kadarını geri veriyorsun */
8663
+ const withMfe = closesAll.filter((e) => typeof e.mfe === 'number').slice(-30);
8664
+ if (withMfe.length >= 3) {
8665
+ const winMfe = withMfe.filter((e) => (Number(e.net) || 0) > 0 && Number(e.mfe) > 0);
8666
+ const sumMfe = winMfe.reduce((a, e) => a + Number(e.mfe), 0);
8667
+ const sumNet = winMfe.reduce((a, e) => a + (Number(e.net) || 0), 0);
8668
+ if (sumMfe > 0) {
8669
+ L.push(`Kâr yakalama: son ${winMfe.length} kazanan işlemde ulaşılan maksimum kârın %${Math.round((sumNet / sumMfe) * 100)}'i realize edildi`);
8670
+ }
8671
+ const losers = withMfe.filter((e) => (Number(e.net) || 0) < 0 && typeof e.mae === 'number');
8672
+ if (losers.length) {
8673
+ const avgMae = losers.reduce((a, e) => a + Math.abs(Number(e.mae) || 0), 0) / losers.length;
8674
+ L.push(`Kaybedenlerde ortalama MAE (en kötü seviye): -${Math.round(avgMae * 100) / 100}`);
8675
+ }
8676
+ }
8677
+ const notes = entries.filter((e) => e.kind === 'note').slice(-3);
8678
+ if (notes.length) {
8679
+ L.push('Son notların: ' + notes.map((n) => `${n.symbol || '?'}: ${String(n.note || '').replace(/\s+/g, ' ').slice(0, 90)}`).join(' | '));
8680
+ }
8681
+ /* Aktif disiplin kilidi varsa ajana açıkça söyle */
8682
+ try {
8683
+ const block = finrisk.disciplineError(entries, Date.now(), f, 'buy', '', []);
8684
+ if (block) L.push('AKTİF KİLİT: ' + block);
8685
+ } catch {}
8686
+ return L.join('\n');
8687
+ }
8688
+
8689
+ /* Koşan ekip ajanlarının son raporlarını trader turuna taşır. */
8690
+ function finTeamDigest() {
8691
+ const L = [];
8692
+ try {
8693
+ for (const [sid, a] of financeState.agents) {
8694
+ if (!a || !a.role || !engine) continue;
8695
+ const s = engine.cache.get(String(sid));
8696
+ const msgs = (s && s.messages) || [];
8697
+ for (let i = msgs.length - 1; i >= 0; i--) {
8698
+ const m = msgs[i];
8699
+ if (m && m.role === 'assistant' && typeof m.content === 'string' && m.content.trim()) {
8700
+ const def = finRoleDef(a.role);
8701
+ L.push(`- ${def ? def.label : a.role}: ${m.content.replace(/\s+/g, ' ').slice(0, 260)}`);
8702
+ break;
8703
+ }
8704
+ }
8705
+ }
8706
+ } catch {}
8707
+ return L.join('\n');
8708
+ }
8709
+
8710
+ /* KODLA DİSİPLİN kancası: financetools işlem öncesi bunu çağırır. */
8711
+ function finDisciplineError(side, symbol, positions) {
8712
+ try {
8713
+ return finrisk.disciplineError(finJournalTail(400), Date.now(), finCfg(), side, symbol, positions);
8714
+ } catch {
8715
+ return null;
8716
+ }
8717
+ }
8718
+
8719
+ /* ---------- MAE/MFE: pozisyonun gördüğü en iyi/en kötü seviye ---------- */
8720
+ function finExcLoad() {
8721
+ try {
8722
+ const raw = finReadJson(finFile('excursions.json'), {});
8723
+ for (const [t, v] of Object.entries(raw || {})) {
8724
+ if (!v || typeof v !== 'object') continue;
8725
+ financeState.excursions.set(String(t), {
8726
+ symbol: String(v.symbol || ''),
8727
+ side: String(v.side || ''),
8728
+ mfe: Number(v.mfe) || 0,
8729
+ mae: Number(v.mae) || 0,
8730
+ at: Number(v.at) || Date.now(),
8731
+ });
8732
+ }
8733
+ } catch {}
8734
+ }
8735
+ function finExcSave() {
8736
+ financeState.excDirty = true;
8737
+ }
8738
+ function finExcFlush(force) {
8739
+ if (!financeState.excDirty) return;
8740
+ const now = Date.now();
8741
+ if (!force && now - financeState.excSavedAt < 15000) return;
8742
+ financeState.excSavedAt = now;
8743
+ financeState.excDirty = false;
8744
+ try {
8745
+ finWriteJson(finFile('excursions.json'), Object.fromEntries(financeState.excursions));
8746
+ } catch {}
8747
+ }
8748
+
8608
8749
  function finAgentInfo(sid) {
8609
8750
  try {
8610
8751
  const a = financeState.agents.get(String(sid || ''));
@@ -8867,7 +9008,14 @@ async function finRecordClose(ticket, st) {
8867
9008
  }
8868
9009
  } catch {}
8869
9010
  const rounded = net == null ? null : Math.round(net * 100) / 100;
8870
- finJournal({ kind: 'close', ticket, symbol, net: rounded, side: (st && st.side) || '' });
9011
+ /* MAE/MFE: pozisyonun gördüğü en iyi/en kötü seviyeler kapanışa iliştirilir */
9012
+ const ex = financeState.excursions.get(String(ticket)) || null;
9013
+ const mfe = ex ? Math.round(Number(ex.mfe) * 100) / 100 : undefined;
9014
+ const mae = ex ? Math.round(Number(ex.mae) * 100) / 100 : undefined;
9015
+ financeState.excursions.delete(String(ticket));
9016
+ finExcSave();
9017
+ finExcFlush(true);
9018
+ finJournal({ kind: 'close', ticket, symbol, net: rounded, side: (st && st.side) || '', mfe, mae });
8871
9019
  const pl = rounded == null ? '' : ` · K/Z ${rounded >= 0 ? '+' : ''}${rounded.toFixed(2)}`;
8872
9020
  const line = `🏁 Pozisyon kapandı: ${symbol || '?'} #${ticket}${pl}`;
8873
9021
  financeLog('[watchdog] ' + line);
@@ -8945,6 +9093,19 @@ async function finWatchTick() {
8945
9093
  for (const p of positions) {
8946
9094
  const ticket = String(p.ticket);
8947
9095
  live.add(ticket);
9096
+ /* MAE/MFE excursion takibi: kâr/zararın gördüğü en iyi-en kötü seviye */
9097
+ {
9098
+ let ex = financeState.excursions.get(ticket);
9099
+ const profit = Number(p.profit) || 0;
9100
+ if (!ex) {
9101
+ ex = { symbol: String(p.symbol || ''), side: Number(p.type) === 0 ? 'buy' : 'sell', mfe: profit, mae: profit, at: Date.now() };
9102
+ financeState.excursions.set(ticket, ex);
9103
+ } else {
9104
+ if (profit > ex.mfe) ex.mfe = profit;
9105
+ if (profit < ex.mae) ex.mae = profit;
9106
+ }
9107
+ finExcSave();
9108
+ }
8948
9109
  let st = financeState.watch.get(ticket);
8949
9110
  if (!st) {
8950
9111
  st = {
@@ -9026,6 +9187,7 @@ async function finWatchTick() {
9026
9187
  finStatsRefresh(true).catch(() => {});
9027
9188
  }
9028
9189
  finMaybeWeeklyReport();
9190
+ finExcFlush(false); /* MAE/MFE deposu 15 sn'de bir diske */
9029
9191
  } catch {
9030
9192
  } finally {
9031
9193
  financeState.watchBusy = false;
@@ -9076,8 +9238,19 @@ async function finWeeklyReport(manual) {
9076
9238
  const positions = posR && posR.ok ? ((posR.data && posR.data.positions) || []) : [];
9077
9239
  const fromTs = now - 7 * 86400000;
9078
9240
  const equity = (financeState.equity || []).filter((p) => Number(p.at) >= fromTs - 86400000);
9079
- const journal = finJournalTail(40);
9241
+ const journal = finJournalTail(400);
9080
9242
  const drawdown = finstats.maxDrawdown(equity);
9243
+ /* MFE/MAE özeti: kâr yakalama oranı ve kaybedenlerde ortalama en kötü seviye */
9244
+ const withMfe = journal.filter((e) => e.kind === 'close' && typeof e.mfe === 'number');
9245
+ const mfeWin = withMfe.filter((e) => (Number(e.net) || 0) > 0 && Number(e.mfe) > 0);
9246
+ const mfeSumMfe = mfeWin.reduce((a, e) => a + Number(e.mfe), 0);
9247
+ const mfeSumNet = mfeWin.reduce((a, e) => a + (Number(e.net) || 0), 0);
9248
+ const mfeLoss = withMfe.filter((e) => (Number(e.net) || 0) < 0 && typeof e.mae === 'number');
9249
+ const mfe = withMfe.length ? {
9250
+ n: withMfe.length,
9251
+ capture: mfeSumMfe > 0 ? Math.round((mfeSumNet / mfeSumMfe) * 100) : null,
9252
+ avgMaeLoss: mfeLoss.length ? Math.round((mfeLoss.reduce((a, e) => a + Math.abs(Number(e.mae)), 0) / mfeLoss.length) * 100) / 100 : null,
9253
+ } : null;
9081
9254
  let review = '';
9082
9255
  if (engine) {
9083
9256
  try {
@@ -9090,7 +9263,7 @@ async function finWeeklyReport(manual) {
9090
9263
  review = String(res || '').trim().slice(0, 4000);
9091
9264
  } catch {}
9092
9265
  }
9093
- const md = finstats.buildWeeklyReport({ stats, drawdown, equity, journal, account, positions, fromTs, toTs: now, review });
9266
+ const md = finstats.buildWeeklyReport({ stats, drawdown, equity, journal, account, positions, fromTs, toTs: now, review, mfe });
9094
9267
  const day = finstats.dayKey(now);
9095
9268
  const file = path.join(financeDir(), 'reports', 'weekly-' + day + '.md');
9096
9269
  try {
@@ -9125,6 +9298,7 @@ function finMaybeWeeklyReport() {
9125
9298
 
9126
9299
  try { finAlertsLoad(); } catch {}
9127
9300
  try { finWatchLoad(); } catch {}
9301
+ try { finExcLoad(); } catch {}
9128
9302
  try { finStatsLoad(); } catch {}
9129
9303
  try { finEquityLoad(); } catch {}
9130
9304
 
@@ -9132,6 +9306,7 @@ try { finEquityLoad(); } catch {}
9132
9306
  try {
9133
9307
  financetools.setConfig(() => finCfg());
9134
9308
  financetools.setAlerts(finAlertApi());
9309
+ financetools.setDiscipline((side, symbol, positions) => finDisciplineError(side, symbol, positions));
9135
9310
  financetools.setNotify((entry) => {
9136
9311
  const k = String((entry && entry.kind) || '');
9137
9312
  const d = entry && entry.data ? entry.data : {};
@@ -9142,15 +9317,23 @@ try {
9142
9317
  finJournal({ kind: 'note', sid, agent: who, symbol: d.symbol, note: d.note, ticket: d.ticket || 0, side: d.side || '' });
9143
9318
  return;
9144
9319
  }
9320
+ /* SHADOW: gerçek emir yok — karar teziyle günlüğe yazılır (test/ölçüm) */
9321
+ if (k === 'shadow') {
9322
+ finJournal({ kind: 'shadow', sid, agent: who, symbol: d.symbol, side: d.side || '', volume: d.volume, sl: d.sl || 0, tp: d.tp || 0, price: d.price || 0, type: d.type || '', reason: d.reason || '' });
9323
+ const sline = `🧪 SHADOW: ${d.side || d.type || ''} ${d.volume || ''} ${d.symbol || ''} — emir gönderilmedi (karar günlüğe yazıldı)`;
9324
+ financeLog('[shadow] ' + sline);
9325
+ finPush('trade', { line: sline });
9326
+ return;
9327
+ }
9145
9328
  if (k === 'trade') {
9146
- finJournal({ kind: 'trade', sid, agent: who, symbol: d.symbol, side: d.side, volume: d.volume, sl: d.sl, tp: d.tp, reason: d.comment || '' });
9329
+ 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 || '' });
9147
9330
  } else if (k === 'close') {
9148
9331
  finJournal({ kind: 'close-req', sid, agent: who, ticket: d.ticket, volume: d.volume });
9149
9332
  return; /* kesin kapanış K/Z'si watchdog tarafından yazılır (çift bildirim yok) */
9150
9333
  } else if (k === 'modify') {
9151
9334
  finJournal({ kind: 'modify', sid, agent: who, ticket: d.ticket, sl: d.sl, tp: d.tp });
9152
9335
  } else if (k === 'pending') {
9153
- finJournal({ kind: 'pending', sid, agent: who, symbol: d.symbol, type: d.type, volume: d.volume });
9336
+ finJournal({ kind: 'pending', sid, agent: who, symbol: d.symbol, type: d.type, volume: d.volume, reason: d.reason || '' });
9154
9337
  } else if (k === 'cancel') {
9155
9338
  finJournal({ kind: 'cancel', sid, agent: who, ticket: d.ticket });
9156
9339
  }
@@ -9196,6 +9379,10 @@ try {
9196
9379
  } catch {}
9197
9380
 
9198
9381
  function financeEnsureBridge() {
9382
+ /* BEAST FINANCE = MT5 KAPISI: mod KAPALIYKEN köprü HİÇ başlatılmaz —
9383
+ Beast, kullanıcı finance açmadıkça MT5 terminalini açmaz/yeniden
9384
+ başlatmaz. Mod açılınca finance:mode handler'ı burayı çağırır. */
9385
+ if (!financeState.mode) return mt5bridge.status();
9199
9386
  const f = finCfg();
9200
9387
  const candidates = [String(f.pythonPath || '').trim(), 'python', 'py -3'].filter(Boolean);
9201
9388
  /* Beast gömülü Python runtime kuruluysa en başa ekle (makinede Python olmasa da köprü çalışır) */
@@ -9343,19 +9530,23 @@ function finAgentRegisterBg(s, symbols) {
9343
9530
  } catch {}
9344
9531
  }
9345
9532
 
9346
- function finApplyTraderFields(s, symbolsOverride, role) {
9533
+ function finApplyTraderFields(s, symbolsOverride, role, isMain) {
9347
9534
  const f = finCfg();
9348
9535
  const r = String(role || s.financeRole || '');
9349
9536
  s.finance = true;
9350
9537
  s.financeTrader = true;
9351
9538
  s.financeRole = r;
9539
+ /* ANA TRADER: playbook skill'i roleSkills.trader'dan gelir */
9540
+ if (typeof isMain === 'boolean') s.financePlaybook = isMain;
9352
9541
  /* rol ajanları ASLA işlem açmaz — trade ajanı ve işçileri DAIMA açabilir */
9353
9542
  s.financeAuto = !r;
9354
9543
  s.financeSymbols = Array.isArray(symbolsOverride) && symbolsOverride.length ? symbolsOverride : f.symbols;
9355
9544
  s.financeStrategy = String(f.strategy || '');
9545
+ s.financeShadow = !!f.shadowMode; /* shadow modda işlem araçları emir göndermez */
9356
9546
  s.financeLimits = { maxLot: f.maxLot, maxPositions: f.maxPositions };
9357
- /* rolün okuması gereken skill'ler (modal eşleştirmesi) — promptta role eklenir */
9358
- s.financeRoleSkills = (f.roleSkills && f.roleSkills[r]) || [];
9547
+ /* ZORUNLU TEK skill: rol ajanı → roleSkills[rol], ana trader → roleSkills.trader */
9548
+ const skillKey = r || (s.financePlaybook ? 'trader' : '');
9549
+ s.financeRoleSkills = (skillKey && f.roleSkills && f.roleSkills[skillKey]) || [];
9359
9550
  engine.cache.set(String(s.id), s);
9360
9551
  }
9361
9552
 
@@ -9384,7 +9575,7 @@ function finAgentCreate(symbols, isMain, role) {
9384
9575
  timer: null,
9385
9576
  };
9386
9577
  financeState.agents.set(String(s.id), record);
9387
- finApplyTraderFields(s, record.symbols, record.role);
9578
+ finApplyTraderFields(s, record.symbols, record.role, !!isMain);
9388
9579
  finAgentRegisterBg(s, record.symbols);
9389
9580
  return { s, agent: record };
9390
9581
  }
@@ -9423,6 +9614,9 @@ function finTraderBrief(agent) {
9423
9614
  const syms = agent && agent.symbols && agent.symbols.length ? agent.symbols.join(', ') : (f.symbols || []).join(', ');
9424
9615
  const roleDef = finRoleDef(agent && agent.role);
9425
9616
  const who = agent && agent.main ? 'TRADER' : roleDef ? roleDef.label.toUpperCase() : 'FINANCE AJANI';
9617
+ const playbook = agent && agent.main && f.roleSkills && f.roleSkills.trader && f.roleSkills.trader[0]
9618
+ ? f.roleSkills.trader[0]
9619
+ : '';
9426
9620
  return [
9427
9621
  `Beast Finance ${who} başlatıldı — ilk tur: strateji çerçeveni kur ve piyasa taramasını yap.`,
9428
9622
  `Odak semboller: ${syms || '(boş — mt5_status ile terminale bak, mantıklı semboller seç)'}`,
@@ -9433,14 +9627,66 @@ function finTraderBrief(agent) {
9433
9627
  roleDef
9434
9628
  ? '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.'
9435
9629
  : f.strategy ? `Sahibinin strateji notu: ${f.strategy}` : 'Strateji notu yok: trend + destek/direnç + momentum ile temel okuma yap.',
9436
- roleDef && f.strategy ? `Sahibinin strateji notu (bu çerçevede analiz et): ${f.strategy}` : '',
9630
+ roleDef && f.strategy ? `Sahibinin strateji notu (bu çerçevede analiz et): ${f.strategy}` : '',
9631
+ playbook ? `PLAYBOOK (ZORUNLU skill): skill("${playbook}") — sistem promptunda tam metni verilmiştir; her turda birebir uygula.` : '',
9632
+ f.shadowMode ? 'SHADOW MOD AÇIK: emir gönderilmez — kararlarını gerekçesiyle raporla, gerçek işlem açılmaz.' : '',
9437
9633
  'Bu turda: mt5_status → hesap/pozisyon/fiyat verisi → mt5_rates/mt5_indicators ile teknik okuma → değerlendirme → kararlar (veya BEKLE: sebep) → kısa rapor.',
9438
9634
  'Önemli kararların gerekçesini mt5_note ile günlüğe yaz (haftalık performans raporu bu notları kullanır).',
9439
9635
  'Risk otomasyonu main süreçte 5 sn döngüyle çalışır (+R BE, trailing, kısmi TP) — sen yine de SL/TP seviyelerini aktif yönet.',
9440
9636
  'Diğer finance/paralel ajanlarla koordinasyon için agent_dm aracı var (to: ajan başlığı anahtar kelimesi).',
9637
+ agent && agent.main && !roleDef ? `İŞLEM GEÇMİŞİN:\n${finBuildDigest()}` : '',
9441
9638
  ].filter(Boolean).join('\n');
9442
9639
  }
9443
9640
 
9641
+ /* GÜNLÜK RUTİN: plan (açılış) / review (kapanış) — cron 'finance-plan' /
9642
+ 'finance-review' job'ları tetikler. Trader koşmuyorsa sessizce atlanır. */
9643
+ function finDailyRoutine(mode) {
9644
+ try {
9645
+ let mainSid = '';
9646
+ for (const [sid, a] of financeState.agents) {
9647
+ if (a && a.main) { mainSid = String(sid); break; }
9648
+ }
9649
+ if (!mainSid || !engine) {
9650
+ financeLog('[rutin] trader koşmuyor — günlük ' + mode + ' atlandı');
9651
+ return;
9652
+ }
9653
+ const agent = financeState.agents.get(mainSid);
9654
+ const s = engine.cache.get(mainSid);
9655
+ if (s) finApplyTraderFields(s, agent.symbols, agent.role, true);
9656
+ if (engine.isBusy(mainSid)) {
9657
+ financeLog('[rutin] trader meşgul — günlük ' + mode + ' atlandı');
9658
+ return;
9659
+ }
9660
+ const digest = finBuildDigest();
9661
+ const text = mode === 'plan'
9662
+ ? 'GÜNLÜK PLAN (açılış rutini): Bugün için net bir plan yap — izleme listesini ve piyasa koşullarını tara; sembol başına yön eğilimi, izlenecek seviyeler ve risk planı (kaç işlem, hangi setup/kurulum, günlük kayıp sınırı) yaz. Zorunlu işlem yok. Planı mt5_note ile günlüğe kaydet.\n\nİŞLEM GEÇMİŞİN:\n' + digest
9663
+ : 'GÜNLÜK REVIEW (kapanış rutini): Bugünün işlemlerini ve performans geçmişini değerlendir — hangi kararlar işledi, hangileri hata; kâr yakalama (MFE) ve MAE verisine bak; açık pozisyonların SL/TP\'lerini kontrol et, korumasız bırakma; yarın için 2-3 somut ders çıkar ve mt5_note ile kaydet.\n\nİŞLEM GEÇMİŞİN:\n' + digest;
9664
+ const ok = engine.send(mainSid, text, { userAction: true });
9665
+ financeLog('[rutin] günlük ' + mode + (ok ? ' gönderildi' : ' gönderilemedi (meşgul)'));
9666
+ } catch {}
9667
+ }
9668
+
9669
+ /* Plan/review saatlerini hafta içi cron job'larına bağla (cron.json kalıcı). */
9670
+ function finSyncScheduleJobs() {
9671
+ try {
9672
+ const f = finCfg();
9673
+ cron.removeIf((j) => j && (j.kind === 'finance-plan' || j.kind === 'finance-review'));
9674
+ const add = (kind, name, time) => {
9675
+ const m = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(String(time || '').trim());
9676
+ if (!m) return;
9677
+ const r = cron.add({
9678
+ name,
9679
+ schedule: `${Number(m[2])} ${Number(m[1])} * * 1-5`,
9680
+ prompt: name,
9681
+ kind,
9682
+ });
9683
+ if (r && r.ok) financeLog('[rutin] ' + name + ' zamanlandı: ' + r.job.schedule + ' (hafta içi)');
9684
+ };
9685
+ add('finance-plan', 'Finance Günlük Plan', f.planTime);
9686
+ add('finance-review', 'Finance Günlük Review', f.reviewTime);
9687
+ } catch {}
9688
+ }
9689
+
9444
9690
  /* Her tur öncesi CHAT AJANINDAN plan iste: aynı motorun odaklı alt-ajanı
9445
9691
  (_subagent) hesap/pozisyon/strateji bağlamıyla kısa işlem planı üretir.
9446
9692
  Plan, tur mesajına [ANA AJAN PLANI] bloğu olarak eklenir. */
@@ -9505,13 +9751,21 @@ function finAgentRound(sid) {
9505
9751
  if (agent.main) financeState.traderRounds = agent.round;
9506
9752
  const f = finCfg();
9507
9753
  const roleDef = finRoleDef(agent.role);
9754
+ let shadow = false;
9508
9755
  try {
9509
9756
  const s = engine.cache.get(String(sid));
9510
- if (s) finApplyTraderFields(s, agent.symbols, agent.role);
9757
+ if (s) {
9758
+ finApplyTraderFields(s, agent.symbols, agent.role, !!agent.main);
9759
+ /* Geri bildirim döngüsü: ana trader her turda kendi işlem geçmişini görür */
9760
+ s.financeDigest = agent.main ? finBuildDigest() : '';
9761
+ shadow = !!s.financeShadow;
9762
+ }
9511
9763
  } catch {}
9512
9764
  const auto = roleDef
9513
9765
  ? `UZMANLIK: ${roleDef.desc} — İŞLEM AÇMA, yalnız analiz + net öneri.`
9514
- : 'İşlem açabilirsin — limitlere uy, SL\u2019siz pozisyon bırakma.';
9766
+ : shadow
9767
+ ? 'SHADOW MOD AÇIK: emir GÖNDERİLMEZ — kararını teziyle raporla (günlüğe yazılır).'
9768
+ : 'İşlem açabilirsin — limitlere uy, SL\u2019siz pozisyon bırakma.';
9515
9769
  const focus = agent.symbols.length ? `Odak: ${agent.symbols.join(', ')}. ` : '';
9516
9770
  const round = `FINANCE TUR #${agent.round}: ${focus}hesap + pozisyonlar + fiyatları çek; ${roleDef ? 'rolüne uygun analiz yap (mt5_rates/mt5_indicators ile) ve öneri ver.' : 'açık pozisyonları yönet (SL/TP güncelle, hedefe ulaşanı kapat); mt5_rates/mt5_indicators ile yeni fırsatları değerlendir.'} ${auto} Önemli kararların gerekçesini mt5_note ile günlüğe yaz. Kısa rapor ver.`;
9517
9771
  const launch = (planBlock) => {
@@ -9526,15 +9780,22 @@ function finAgentRound(sid) {
9526
9780
  agent.timer = setTimeout(() => { try { finAgentRound(sid); } catch {} }, 30000);
9527
9781
  }
9528
9782
  };
9783
+ /* EKİP ENTEGRASYONU: ana trader turuna koşan uzman ajanların son raporları
9784
+ enjekte edilir — karar öncesi görülmesi garanti edilir. */
9785
+ let teamPrefix = '';
9786
+ if (agent.main) {
9787
+ const team = finTeamDigest();
9788
+ if (team) teamPrefix = '[EKİP RAPORLARI — koşan uzman ajanların son raporları; kararında dikkate al]\n' + team + '\n\n';
9789
+ }
9529
9790
  if (f.consultChat === false || roleDef) {
9530
9791
  /* danışma kapalı ya da rol ajanı (plan trader'a yöneliktir): tur doğrudan başlar */
9531
- launch('');
9792
+ launch(teamPrefix);
9532
9793
  return;
9533
9794
  }
9534
9795
  if (agent.main) finPush('trader', { state: 'consult', round: agent.round });
9535
9796
  finConsultPlan(f, agent, String(sid))
9536
- .then((plan) => launch(plan ? `[ANA AJAN PLANI — bu turun varsayılan stratejisi; strateji notuyla çelişirse not önceliklidir]\n${plan}\n\n` : ''))
9537
- .catch(() => launch(''));
9797
+ .then((plan) => launch(teamPrefix + (plan ? `[ANA AJAN PLANI — bu turun varsayılan stratejisi; strateji notuyla çelişirse not önceliklidir]\n${plan}\n\n` : '')))
9798
+ .catch(() => launch(teamPrefix));
9538
9799
  }
9539
9800
 
9540
9801
  /* Tur/durum sonları: sürekli ajan döngüsünü besle; kullanıcı iptalinde kapat */
@@ -9616,6 +9877,20 @@ ipcMain.handle('finance:snapshot', async () => {
9616
9877
 
9617
9878
  ipcMain.handle('finance:mode', async (_e, payload) => {
9618
9879
  financeState.mode = !!(payload && payload.on);
9880
+ /* BEAST FINANCE = MT5 KAPISI:
9881
+ - mod AÇILINCA köprü + MT5 terminali OTOMATİK başlar (watchdog dahil) —
9882
+ panel açılır açılmaz bağlantı kurulur, snapshot'ı beklemez.
9883
+ - mod KAPANINCA otomatik başlatma İZNİ kapanır: köprü/watchdog çalışmaya
9884
+ devam eder (açık MT5'te koruma sürer) ama MT5 kapalıysa Beast onu ARTIK
9885
+ açmaz/yeniden başlatmaz. Açık terminal KAPATILMAZ (manuel işlemlere ve
9886
+ açık pozisyonlara dokunulmaz); finance tekrar açılınca MT5 gerekirse
9887
+ yeniden otomatik başlar. */
9888
+ if (financeState.mode) {
9889
+ try { financeEnsureBridge(); } catch {}
9890
+ try { await mt5bridge.call('policy', { launch: true }, 4000); } catch {}
9891
+ } else {
9892
+ try { await mt5bridge.call('policy', { launch: false }, 4000); } catch {}
9893
+ }
9619
9894
  const sid = String((payload && payload.sessionId) || '');
9620
9895
  let needNew = false;
9621
9896
  let resumeSid = '';
@@ -9731,11 +10006,12 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
9731
10006
  if (p.analysisTeam !== undefined) f.analysisTeam = finRolesValid(p.analysisTeam);
9732
10007
  if (p.analysisAuto !== undefined) f.analysisAuto = !!p.analysisAuto;
9733
10008
  if (p.analysisCount !== undefined) f.analysisCount = Math.max(0, Math.min(FIN_ROLES_AUTO.length, Math.round(Number(p.analysisCount) || 0)));
9734
- /* ROL → SKILL eşleştirmesi güncellemesi (modal): rol başına TEK skill */
10009
+ /* ROL → SKILL eşleştirmesi güncellemesi (modal): rol başına TEK skill +
10010
+ ana trader playbook'u (trader) */
9735
10011
  if (p.roleSkills !== undefined && p.roleSkills && typeof p.roleSkills === 'object') {
9736
- for (const d of FIN_ROLES) {
9737
- if (p.roleSkills[d.id] === undefined) continue;
9738
- f.roleSkills[d.id] = (Array.isArray(p.roleSkills[d.id]) ? p.roleSkills[d.id] : [])
10012
+ for (const id of [...FIN_ROLES.map((d) => d.id), 'trader']) {
10013
+ if (p.roleSkills[id] === undefined) continue;
10014
+ f.roleSkills[id] = (Array.isArray(p.roleSkills[id]) ? p.roleSkills[id] : [])
9739
10015
  .map((s) => String(s || '').trim()).filter(Boolean).slice(0, 1);
9740
10016
  }
9741
10017
  /* koşan rol ajanlarını da tazele — seçim sonraki turdan itibaren sistem
@@ -9749,6 +10025,15 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
9749
10025
  }
9750
10026
  }
9751
10027
  }
10028
+ /* KODLA DİSİPLİN + SHADOW + GÜNLÜK RUTİN ayarları */
10029
+ if (p.maxTradesPerDay !== undefined) f.maxTradesPerDay = Math.max(0, Math.min(50, Math.round(Number(p.maxTradesPerDay) || 0)));
10030
+ if (p.lossStreakLimit !== undefined) f.lossStreakLimit = Math.max(0, Math.min(10, Math.round(Number(p.lossStreakLimit) || 0)));
10031
+ if (p.lossStreakPauseMin !== undefined) f.lossStreakPauseMin = Math.max(0, Math.min(1440, Math.round(Number(p.lossStreakPauseMin) || 0)));
10032
+ if (p.reentryCooldownMin !== undefined) f.reentryCooldownMin = Math.max(0, Math.min(1440, Math.round(Number(p.reentryCooldownMin) || 0)));
10033
+ if (p.maxPerCurrency !== undefined) f.maxPerCurrency = Math.max(0, Math.min(20, Math.round(Number(p.maxPerCurrency) || 0)));
10034
+ if (p.shadowMode !== undefined) f.shadowMode = !!p.shadowMode;
10035
+ if (p.planTime !== undefined) f.planTime = /^([01]?\d|2[0-3]):([0-5]\d)$/.test(String(p.planTime || '').trim()) ? String(p.planTime).trim() : '';
10036
+ if (p.reviewTime !== undefined) f.reviewTime = /^([01]?\d|2[0-3]):([0-5]\d)$/.test(String(p.reviewTime || '').trim()) ? String(p.reviewTime).trim() : '';
9752
10037
  if (p.pythonPath !== undefined) f.pythonPath = String(p.pythonPath || '').trim();
9753
10038
  if (p.terminalPath !== undefined) f.terminalPath = String(p.terminalPath || '').trim();
9754
10039
  if (p.traderSel !== undefined) {
@@ -9762,7 +10047,7 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
9762
10047
  if (financeState.traderSid && engine) {
9763
10048
  try {
9764
10049
  const ts = engine.cache.get(financeState.traderSid);
9765
- if (ts) finApplyTraderFields(ts);
10050
+ if (ts) finApplyTraderFields(ts, undefined, undefined, true);
9766
10051
  } catch {}
9767
10052
  }
9768
10053
  /* finance SOHBET oturumları: strateji/sembol/limit/rol-skill değişince ANINDA
@@ -9777,6 +10062,8 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
9777
10062
  }
9778
10063
  } catch {}
9779
10064
  }
10065
+ /* günlük plan/review saatleri değiştiyse cron job'larını eşitle */
10066
+ if (p.planTime !== undefined || p.reviewTime !== undefined) finSyncScheduleJobs();
9780
10067
  return { ok: true, cfg: f };
9781
10068
  });
9782
10069
 
@@ -9786,6 +10073,7 @@ async function financeTraderStart() {
9786
10073
  if (!engine) return { ok: false, error: 'ajan hazır değil' };
9787
10074
  const f = finCfg();
9788
10075
  if (!engine.publicState().hasModel && !f.traderSel) return { ok: false, error: 'model yok — Ayarlar → Provider' };
10076
+ try { finSyncScheduleJobs(); } catch {} /* plan/review saatleri trader açılışında garantiye alınır */
9789
10077
  /* ANA trader: varsa aynen sürdür, yoksa yeni sürekli ajan aç */
9790
10078
  let mainSid = '';
9791
10079
  let mainAgent = null;
@@ -190,6 +190,22 @@
190
190
  <span>Ajan talimatı <i class="fin-hint">— 1-2 cümle; trader + tüm paralel ajanlara iletilir</i></span>
191
191
  <textarea id="finStrategy" rows="2" maxlength="2000" placeholder="örn. Her türde scalp yap — M1/M5 hızlı gir-çık, günlük kâr hedefine ulaşınca dur" title="Buraya yazdığın talimat trade ajanına ve koşan tüm paralel ajanlara (ekip + sembol işçileri) sistem promptunda öncelikli strateji notu olarak gider; kaydetmek için alandan çık"></textarea>
192
192
  </div>
193
+ <div class="fin-field">
194
+ <span>Disiplin <i class="fin-hint">— kodla zorlanır; 0 = kural kapalı</i></span>
195
+ <div class="fin-grid2">
196
+ <div class="fin-field"><span>Günlük max işlem</span><input id="finMaxTradesDay" type="number" min="0" max="50" step="1" title="Bir günde en fazla kaç işlem açılabilir (0 = sınırsız)" /></div>
197
+ <div class="fin-field"><span>Kayıp serisi limiti</span><input id="finLossStreak" type="number" min="0" max="10" step="1" title="Üst üste bu kadar kayıptan sonra yeni işlem kilidi (0 = kapalı)" /></div>
198
+ <div class="fin-field"><span>Seri molası (dk)</span><input id="finLossStreakPause" type="number" min="0" max="1440" step="5" title="Kayıp serisi limitine ulaşınca kaç dakika yeni işlem açılmaz" /></div>
199
+ <div class="fin-field"><span>Re-entry bekleme (dk)</span><input id="finReentry" type="number" min="0" max="1440" step="5" title="Aynı sembolda bir işlem kapandıktan sonra yeni giriş için beklenecek süre (0 = kapalı)" /></div>
200
+ <div class="fin-field"><span>Kur başına max</span><input id="finMaxPerCurrency" type="number" min="0" max="20" step="1" title="Yönlü kur maruziyeti (korelasyon) limiti — örn. 3: aynı para biriminde en fazla ±3 net pozisyon (0 = kapalı)" /></div>
201
+ <div class="fin-field"><span>Plan / Review (saat)</span><div class="fin-time-row"><input id="finPlanTime" type="time" title="Hafta içi günlük plan saati (boş = kapalı)" /><input id="finReviewTime" type="time" title="Hafta içi günlük review saati (boş = kapalı)" /></div></div>
202
+ </div>
203
+ </div>
204
+ <div class="fin-field fin-check">
205
+ <label title="SHADOW MOD: emir gönderilmez — kararlar teziyle günlüğe yazılır, gerçek işlem açılmaz. Ajanı para riske atmadan canlı test etmek için.">
206
+ <input id="finShadow" type="checkbox" /> Shadow mod — emir açmadan karar kaydı (canlı test)
207
+ </label>
208
+ </div>
193
209
  <div class="fin-row fin-btn-row">
194
210
  <button id="finTraderBtn" class="fin-start">&#9654;&#xFE0E; Ajanı Başlat</button>
195
211
  </div>
@@ -186,6 +186,14 @@ const els = {
186
186
  finInterval: $('#finInterval'),
187
187
  finMaxLot: $('#finMaxLot'),
188
188
  finStrategy: $('#finStrategy'),
189
+ finMaxTradesDay: $('#finMaxTradesDay'),
190
+ finLossStreak: $('#finLossStreak'),
191
+ finLossStreakPause: $('#finLossStreakPause'),
192
+ finReentry: $('#finReentry'),
193
+ finMaxPerCurrency: $('#finMaxPerCurrency'),
194
+ finPlanTime: $('#finPlanTime'),
195
+ finReviewTime: $('#finReviewTime'),
196
+ finShadow: $('#finShadow'),
189
197
  finSymBtn: $('#finSymBtn'),
190
198
  finRoles: $('#finRoles'),
191
199
  finWatchClear: $('#finWatchClear'),
@@ -7043,7 +7051,9 @@ async function init() {
7043
7051
  els.input.focus();
7044
7052
  });
7045
7053
 
7046
- /* in-app confirm modal düğmeleri + Enter/Esc kısayolları */
7054
+ /* in-app confirm modal düğmeleri + Enter/Esc kısayolları.
7055
+ KRİTİK: Enter'da preventDefault ŞART — closeConfirm odağı sohbet input'una
7056
+ geri verdiği için varsayılan Enter aksiyonu input'a satır atlatıyordu. */
7047
7057
  {
7048
7058
  const ok = $('#confirmOk');
7049
7059
  const cancel = $('#confirmCancel');
@@ -7052,9 +7062,16 @@ async function init() {
7052
7062
  ok.addEventListener('click', () => closeConfirm(true));
7053
7063
  cancel.addEventListener('click', () => closeConfirm(false));
7054
7064
  ov.addEventListener('keydown', (e) => {
7055
- if (e.key === 'Escape') { e.stopPropagation(); closeConfirm(false); }
7056
- else if (e.key === 'Enter') { e.stopPropagation(); closeConfirm(true); }
7065
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeConfirm(false); }
7066
+ else if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); closeConfirm(true); }
7057
7067
  });
7068
+ /* modal AÇIKKEN odak nerede olursa olsun Enter/Esc modal'a gider —
7069
+ sohbet input'u Enter ile satır atlamaz (capture: input'a ulaşmadan kes) */
7070
+ document.addEventListener('keydown', (e) => {
7071
+ if (ov.hidden) return;
7072
+ if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); closeConfirm(true); }
7073
+ else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeConfirm(false); }
7074
+ }, true);
7058
7075
  }
7059
7076
  }
7060
7077
 
@@ -9326,6 +9343,14 @@ function finTraderInputsSet(cfg) {
9326
9343
  if (els.finInterval && ae !== els.finInterval) els.finInterval.value = cfg.intervalSec || 120;
9327
9344
  if (els.finMaxLot && ae !== els.finMaxLot) els.finMaxLot.value = cfg.maxLot || 0.1;
9328
9345
  if (els.finStrategy && ae !== els.finStrategy && !finStrategyDirty) els.finStrategy.value = cfg.strategy || '';
9346
+ if (els.finMaxTradesDay && ae !== els.finMaxTradesDay) els.finMaxTradesDay.value = Number(cfg.maxTradesPerDay) || 0;
9347
+ if (els.finLossStreak && ae !== els.finLossStreak) els.finLossStreak.value = Number(cfg.lossStreakLimit) || 0;
9348
+ if (els.finLossStreakPause && ae !== els.finLossStreakPause) els.finLossStreakPause.value = Number(cfg.lossStreakPauseMin) || 0;
9349
+ if (els.finReentry && ae !== els.finReentry) els.finReentry.value = Number(cfg.reentryCooldownMin) || 0;
9350
+ if (els.finMaxPerCurrency && ae !== els.finMaxPerCurrency) els.finMaxPerCurrency.value = Number(cfg.maxPerCurrency) || 0;
9351
+ if (els.finPlanTime && ae !== els.finPlanTime) els.finPlanTime.value = String(cfg.planTime || '');
9352
+ if (els.finReviewTime && ae !== els.finReviewTime) els.finReviewTime.value = String(cfg.reviewTime || '');
9353
+ if (els.finShadow && ae !== els.finShadow) els.finShadow.checked = !!cfg.shadowMode;
9329
9354
  finSymBtnUpdate();
9330
9355
  if (els.finTraderModel && ae !== els.finTraderModel && cfg.traderSel) els.finTraderModel.value = cfg.traderSel;
9331
9356
  }
@@ -9908,6 +9933,31 @@ finAutoNum(els.finRiskPct, 'riskPerTradePct', 0, 20, 0.1);
9908
9933
  finAutoNum(els.finMaxPerSymbol, 'maxPerSymbol', 0, 20, 1);
9909
9934
  finAutoNum(els.finMaxSameSide, 'maxSameSide', 0, 20, 1);
9910
9935
  finAutoNum(els.finMinMarginLevel, 'minMarginLevel', 0, 1000, 10);
9936
+ /* KODLA DİSİPLİN + shadow + günlük plan/review — anında kaydedilir */
9937
+ finAutoNum(els.finMaxTradesDay, 'maxTradesPerDay', 0, 50, 1);
9938
+ finAutoNum(els.finLossStreak, 'lossStreakLimit', 0, 10, 1);
9939
+ finAutoNum(els.finLossStreakPause, 'lossStreakPauseMin', 0, 1440, 5);
9940
+ finAutoNum(els.finReentry, 'reentryCooldownMin', 0, 1440, 5);
9941
+ finAutoNum(els.finMaxPerCurrency, 'maxPerCurrency', 0, 20, 1);
9942
+ const finTimeSave = (el, key) => {
9943
+ if (!el) return;
9944
+ el.addEventListener('change', () => {
9945
+ finSaveCfg({ [key]: String(el.value || '').slice(0, 5) });
9946
+ toast(key === 'planTime'
9947
+ ? (el.value ? 'Günlük plan saati: ' + el.value + ' (hafta içi)' : 'Günlük plan kapatıldı')
9948
+ : (el.value ? 'Günlük review saati: ' + el.value + ' (hafta içi)' : 'Günlük review kapatıldı'));
9949
+ });
9950
+ };
9951
+ finTimeSave(els.finPlanTime, 'planTime');
9952
+ finTimeSave(els.finReviewTime, 'reviewTime');
9953
+ if (els.finShadow) {
9954
+ els.finShadow.addEventListener('change', () => {
9955
+ finSaveCfg({ shadowMode: els.finShadow.checked });
9956
+ toast(els.finShadow.checked
9957
+ ? 'SHADOW MOD açık — emir gönderilmez, kararlar günlüğe yazılır'
9958
+ : 'Shadow mod kapalı — gerçek emirler aktif');
9959
+ });
9960
+ }
9911
9961
  if (els.finDailyLossAction) {
9912
9962
  els.finDailyLossAction.addEventListener('change', () => {
9913
9963
  finSaveCfg({ dailyLossAction: els.finDailyLossAction.value });
@@ -10156,7 +10206,10 @@ function finRenderSkills() {
10156
10206
  if (!box) return;
10157
10207
  const cfg = finCfgCache || {};
10158
10208
  const map = cfg.roleSkills && typeof cfg.roleSkills === 'object' ? cfg.roleSkills : {};
10159
- const roles = Array.isArray(finRolesCatalog) ? finRolesCatalog : [];
10209
+ /* ANA KARAR VERİCİ (trader) da bir satır: playbook skill'i zorunlu tek seçim */
10210
+ const roles = [
10211
+ { id: 'trader', label: 'TRADER (Ana Karar Verici)', desc: 'playbook: hangi setup\'lar serbest, giriş/çıkış/iptal kuralları, seans ve risk disiplini — asıl karar vericinin zorunlu skill\'i' },
10212
+ ].concat(Array.isArray(finRolesCatalog) ? finRolesCatalog : []);
10160
10213
  box.innerHTML = '';
10161
10214
  if (!roles.length) {
10162
10215
  box.innerHTML = '<div class="fin-empty">Rol kataloğu yok</div>';
@@ -4873,6 +4873,8 @@ body.finance-mode #composerWrap { margin-right: var(--finW, 400px); }
4873
4873
  border-radius: 6px;
4874
4874
  }
4875
4875
  .fin-field input:focus, .fin-field select:focus, .fin-field textarea:focus { border-color: var(--accent); outline: none; }
4876
+ .fin-time-row { display: flex; gap: 6px; }
4877
+ .fin-time-row input { flex: 1; min-width: 0; }
4876
4878
  /* Ajan talimatı: çok satırlı not alanı (dikey büyütülebilir) */
4877
4879
  .fin-field textarea {
4878
4880
  resize: vertical;