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