beast-agent 2.48.0 → 2.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/customtools.js +62 -5
- package/src/agent/defaulttools/mt5_shot/run.js +217 -76
- package/src/agent/defaulttools/mt5_shot/stitch.js +125 -0
- package/src/agent/defaulttools/mt5_shot/tool.json +10 -6
- package/src/agent/engine.js +3 -3
- package/src/main.js +71 -46
- package/src/renderer/index.html +1 -1
- package/src/renderer/renderer.js +25 -2
package/package.json
CHANGED
package/src/agent/customtools.js
CHANGED
|
@@ -20,6 +20,14 @@ function setNotify(fn) {
|
|
|
20
20
|
notify = typeof fn === 'function' ? fn : () => {};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/* KİŞİSEL MT5 TOOLLARI köprüyü doğrudan çağırır (iç mt5_* handler'ı devreye
|
|
24
|
+
girmez) — başarılı çağrıyı trade olayı olarak ana sürece bildir ki jurnal +
|
|
25
|
+
panel/kanal bildirimi (işlem açıldı, SL/TP güncellendi…) kaçmasın. */
|
|
26
|
+
let tradeHook = null;
|
|
27
|
+
function setTradeHook(fn) {
|
|
28
|
+
tradeHook = typeof fn === 'function' ? fn : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
function dir() {
|
|
24
32
|
return path.join(beastRoot(), 'tools');
|
|
25
33
|
}
|
|
@@ -117,7 +125,7 @@ function names() {
|
|
|
117
125
|
const TOOL_TIMEOUT_MS = 90 * 1000;
|
|
118
126
|
|
|
119
127
|
/* Tool'u çocuk node prosesinde koşturur — uygulama asla çökmez */
|
|
120
|
-
function call(name, args) {
|
|
128
|
+
function call(name, args, sid) {
|
|
121
129
|
return new Promise((resolve) => {
|
|
122
130
|
const id = String(name || '').replace(/^tool__/, '');
|
|
123
131
|
if (!id) return resolve({ ok: false, error: 'tool adı gerekli' });
|
|
@@ -142,6 +150,9 @@ function call(name, args) {
|
|
|
142
150
|
error: r && r.error ? String(r.error).slice(0, 200) : null,
|
|
143
151
|
});
|
|
144
152
|
} catch {}
|
|
153
|
+
try {
|
|
154
|
+
if (tradeHook) tradeHook(id, args || {}, r || {}, sid ? String(sid) : '');
|
|
155
|
+
} catch {}
|
|
145
156
|
resolve(r);
|
|
146
157
|
};
|
|
147
158
|
let killer = setTimeout(() => {
|
|
@@ -154,6 +165,7 @@ function call(name, args) {
|
|
|
154
165
|
const env = {
|
|
155
166
|
...process.env,
|
|
156
167
|
BEAST_ROOT: beastRoot(),
|
|
168
|
+
BEAST_APP_DIR: path.join(__dirname, '..', '..'), // tool'lar uygulama node_modules'ına (ör. @napi-rs/canvas) buradan erişir
|
|
157
169
|
BEAST_TOOL_ID: id,
|
|
158
170
|
BEAST_TOOL_ARGS: JSON.stringify(args || {}),
|
|
159
171
|
BEAST_FINANCE_DIR: path.join(beastRoot(), 'finance'),
|
|
@@ -245,6 +257,37 @@ function copyTree(srcDir, dstDir) {
|
|
|
245
257
|
}
|
|
246
258
|
}
|
|
247
259
|
|
|
260
|
+
function readJsonSafe(fp) {
|
|
261
|
+
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* güncelleme öncesi: kullanıcının düzenlediği dosyaları .user-bak olarak sakla */
|
|
265
|
+
function backupEditedFiles(fromDir, toDir) {
|
|
266
|
+
try {
|
|
267
|
+
for (const e of fs.readdirSync(fromDir, { withFileTypes: true })) {
|
|
268
|
+
if (!e.isDirectory()) continue;
|
|
269
|
+
backupEditedFiles(path.join(fromDir, e.name), path.join(toDir, e.name));
|
|
270
|
+
}
|
|
271
|
+
} catch {}
|
|
272
|
+
try {
|
|
273
|
+
for (const e of fs.readdirSync(fromDir, { withFileTypes: true })) {
|
|
274
|
+
if (e.isDirectory()) continue;
|
|
275
|
+
if (e.name === 'tool.json') continue; /* sürüm damgası her güncellemede değişir */
|
|
276
|
+
const from = path.join(fromDir, e.name);
|
|
277
|
+
const to = path.join(toDir, e.name);
|
|
278
|
+
try {
|
|
279
|
+
if (!fs.existsSync(to)) continue;
|
|
280
|
+
if (fs.readFileSync(from).equals(fs.readFileSync(to))) continue;
|
|
281
|
+
const bak = to + '.user-bak';
|
|
282
|
+
if (!fs.existsSync(bak)) fs.copyFileSync(to, bak);
|
|
283
|
+
} catch {}
|
|
284
|
+
}
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* SÜRÜM GÜNCELLEMESİ: shipped tool.json'da "defaultsVersion" varsa ve kurulu
|
|
289
|
+
sürümden büyükse araç güncellenir (ör. mt5_shot çoklu periyot); düzenlenmiş
|
|
290
|
+
dosyalar .user-bak olarak yedeklenir. Alan yoksa eski davranış: yalnız eksikse kur. */
|
|
248
291
|
function seedDefaults() {
|
|
249
292
|
const src = path.join(__dirname, 'defaulttools');
|
|
250
293
|
try {
|
|
@@ -253,13 +296,27 @@ function seedDefaults() {
|
|
|
253
296
|
let n = 0;
|
|
254
297
|
for (const e of fs.readdirSync(src, { withFileTypes: true })) {
|
|
255
298
|
if (!e.isDirectory()) continue;
|
|
299
|
+
const from = path.join(src, e.name);
|
|
256
300
|
const to = path.join(dir(), e.name);
|
|
301
|
+
const shipped = readJsonSafe(path.join(from, 'tool.json'));
|
|
302
|
+
const shippedV = Number(shipped && shipped.defaultsVersion) || 0;
|
|
257
303
|
/* tam kurulu mu? tool.json + run.js yoksa yarım demektir → yeniden kur */
|
|
258
304
|
const complete = fs.existsSync(path.join(to, 'tool.json')) && fs.existsSync(path.join(to, 'run.js'));
|
|
259
|
-
|
|
305
|
+
let need = !complete;
|
|
306
|
+
if (complete && shippedV > 0) {
|
|
307
|
+
const installed = readJsonSafe(path.join(to, 'tool.json'));
|
|
308
|
+
const installedV = Number(installed && installed.defaultsVersion) || 0;
|
|
309
|
+
if (shippedV > installedV) need = true;
|
|
310
|
+
}
|
|
311
|
+
if (!need) continue;
|
|
260
312
|
try {
|
|
261
|
-
if (
|
|
262
|
-
|
|
313
|
+
if (complete) {
|
|
314
|
+
backupEditedFiles(from, to); /* kullanıcı düzenlemesi kaybolmasın */
|
|
315
|
+
copyTree(from, to); /* güncelle: shots/ ve ek dosyalar korunur */
|
|
316
|
+
} else {
|
|
317
|
+
if (fs.existsSync(to)) fs.rmSync(to, { recursive: true, force: true });
|
|
318
|
+
copyTree(from, to);
|
|
319
|
+
}
|
|
263
320
|
n++;
|
|
264
321
|
} catch {}
|
|
265
322
|
}
|
|
@@ -299,4 +356,4 @@ function seedIfEmpty() {
|
|
|
299
356
|
} catch {}
|
|
300
357
|
}
|
|
301
358
|
|
|
302
|
-
module.exports = { setNotify, list, scan, definitions, names, call, save, remove, seedIfEmpty, seedDefaults, invalidate, dir, slugify };
|
|
359
|
+
module.exports = { setNotify, setTradeHook, list, scan, definitions, names, call, save, remove, seedIfEmpty, seedDefaults, invalidate, dir, slugify };
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
/* mt5_shot — MT5 grafiğinden GERÇEK PNG screenshot (BeastFinance EA "shot" komutu)
|
|
3
3
|
stdin: JSON args -> stdout: SADECE JSON sonuç
|
|
4
4
|
|
|
5
|
-
Girdi : {symbol?, timeframe?, width?, height?, file?, template?, timeoutSec?, outdir?, diag?, embed?}
|
|
5
|
+
Girdi : {symbol?, timeframe?, timeframes?:["M1","M15"], layout?:"h"|"v", width?, height?, file?, template?, timeoutSec?, outdir?, diag?, embed?}
|
|
6
6
|
Çıktı : {ok:true, path, symbol, timeframe, ...} | {ok:false, err, error, cmdId, filesDir, ...}
|
|
7
|
-
err kodları: no_terminal | ea_offline | cmd_write_fail | ea_no_ack | ea_error | png_yok | timeout
|
|
7
|
+
err kodları: no_terminal | ea_offline | cmd_write_fail | ea_no_ack | ea_error | png_yok | timeout | stitch_fail | stitch_write_fail
|
|
8
|
+
|
|
9
|
+
ÇOKLU PERİYOT: timeframes:["M1","M15"] (2-3 adet) verilirse her periyot ayrı
|
|
10
|
+
çekilir ve TEK PNG'de birleştirilir (layout:"h" soldan sağa — varsayılan,
|
|
11
|
+
"v" üstten alta). Panellerin sol üstüne "SEMBOL · PERİYOT" etiketi çizilir;
|
|
12
|
+
sonuçta panels[{timeframe, position, path}] hangi tarafın hangi periyot
|
|
13
|
+
olduğunu söyler. tf2:"M15" kısayolu da (timeframe + tf2) kabul edilir.
|
|
8
14
|
|
|
9
15
|
Notlar:
|
|
10
16
|
- beast_cmd.json'a {id, ts, cmd, params} yazılır (UTF-16 BOM; EA FILE_UNICODE okur).
|
|
@@ -159,89 +165,75 @@ function eaPing(filesDir, budgetMs) {
|
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
/* ---------------- ana akış ---------------- */
|
|
162
|
-
function run() {
|
|
163
|
-
const timeoutSec = Number(ARGS.timeoutSec) > 0
|
|
164
|
-
? Math.max(5, Math.min(70, Math.round(Number(ARGS.timeoutSec))))
|
|
165
|
-
: DEFAULT_TIMEOUT_SEC;
|
|
166
|
-
const diag = ARGS.diag !== false;
|
|
167
|
-
|
|
168
|
-
const term = findTerminalData();
|
|
169
|
-
if (!term) return fail('no_terminal', 'MT5 terminal veri klasörü bulunamadı (beast_ea.json yok) — MT5 kurulu/çalışıyor mu?');
|
|
170
|
-
const filesDir = term.files;
|
|
171
|
-
CTX.filesDir = filesDir;
|
|
172
168
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
/* çoklu periyot listesi: ["M1","M15"] ya da "M1,M15"; tf2 kısayolu destekli */
|
|
170
|
+
function normalizeTfs(a) {
|
|
171
|
+
let t = a.timeframes;
|
|
172
|
+
if ((t == null || t === '') && a.tf2) t = [a.timeframe, a.tf2];
|
|
173
|
+
if (typeof t === 'string') t = t.split(/[,\s|+/]+/);
|
|
174
|
+
if (!Array.isArray(t)) return [];
|
|
175
|
+
const out = [];
|
|
176
|
+
for (const x of t) {
|
|
177
|
+
const s = String(x == null ? '' : x).trim().toUpperCase();
|
|
178
|
+
if (s && !out.includes(s)) out.push(s);
|
|
180
179
|
}
|
|
180
|
+
return out.slice(0, 3); /* 80 sn watchdog bütçesi: en fazla 3 panel */
|
|
181
|
+
}
|
|
181
182
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
let file = (ARGS.file && String(ARGS.file).trim()) ? String(ARGS.file).trim() : ('beast_shot_' + id + '.png');
|
|
189
|
-
file = path.basename(file); // yol kaçışını engelle
|
|
190
|
-
if (!/\.(png|gif|bmp|jpg|jpeg)$/i.test(file)) file += '.png';
|
|
191
|
-
|
|
192
|
-
const params = { file };
|
|
193
|
-
if (ARGS.timeframe) params.timeframe = String(ARGS.timeframe).toUpperCase();
|
|
194
|
-
if (ARGS.symbol) params.symbol = String(ARGS.symbol).toUpperCase();
|
|
195
|
-
if (ARGS.template !== undefined) params.template = String(ARGS.template);
|
|
196
|
-
params.width = Number(ARGS.width) > 0 ? Math.min(10000, Math.round(Number(ARGS.width))) : 1600;
|
|
197
|
-
params.height = Number(ARGS.height) > 0 ? Math.min(10000, Math.round(Number(ARGS.height))) : 900;
|
|
198
|
-
|
|
199
|
-
const payload = { id, ts: Date.now(), cmd: 'shot', params };
|
|
183
|
+
function sanitizeFile(name, fallback) {
|
|
184
|
+
let f = (name && String(name).trim()) ? path.basename(String(name).trim()) : fallback;
|
|
185
|
+
if (!/\.(png|gif|bmp|jpg|jpeg)$/i.test(f)) f += '.png';
|
|
186
|
+
return f;
|
|
187
|
+
}
|
|
200
188
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
189
|
+
/* tek kare: EA'ya shot komutu yaz → ack bekle → PNG'yi bekle */
|
|
190
|
+
function captureOne(o) {
|
|
191
|
+
CTX.cmdId = o.id;
|
|
192
|
+
const payload = { id: o.id, ts: Date.now(), cmd: 'shot', params: o.params };
|
|
193
|
+
try { if (fs.existsSync(o.ackFile)) fs.unlinkSync(o.ackFile); } catch (e) {}
|
|
194
|
+
try { writeUtf16(o.cmdFile, JSON.stringify(payload)); }
|
|
195
|
+
catch (e) { return { ok: false, err: 'cmd_write_fail', error: 'beast_cmd.json yazılamadı: ' + e.message, extra: { sent: payload } }; }
|
|
204
196
|
|
|
205
197
|
/* --- ack POLL (cmd dosyasının EA tarafından tüketilmesi de izlenir) --- */
|
|
206
198
|
let ack = null, cmdConsumed = false;
|
|
207
|
-
const ackDeadline = Date.now() + timeoutSec * 1000;
|
|
199
|
+
const ackDeadline = Date.now() + o.timeoutSec * 1000;
|
|
208
200
|
while (Date.now() < ackDeadline) {
|
|
209
201
|
syncSleep(250);
|
|
210
|
-
const a = readJson(ackFile);
|
|
211
|
-
if (a && a.id === id) { ack = a; break; }
|
|
202
|
+
const a = readJson(o.ackFile);
|
|
203
|
+
if (a && a.id === o.id) { ack = a; break; }
|
|
212
204
|
try {
|
|
213
|
-
if (!fs.existsSync(cmdFile)) cmdConsumed = true;
|
|
205
|
+
if (!fs.existsSync(o.cmdFile)) cmdConsumed = true;
|
|
214
206
|
else {
|
|
215
|
-
const c = readJson(cmdFile);
|
|
216
|
-
if (c && c.id && c.id !== id) cmdConsumed = true; // başka bir çağrı ezip geçti
|
|
207
|
+
const c = readJson(o.cmdFile);
|
|
208
|
+
if (c && c.id && c.id !== o.id) cmdConsumed = true; // başka bir çağrı ezip geçti
|
|
217
209
|
}
|
|
218
210
|
} catch (e) {}
|
|
219
211
|
}
|
|
220
212
|
|
|
221
213
|
if (!ack) {
|
|
222
214
|
const extra = {
|
|
223
|
-
heartbeat_age_sec: heartbeatAge(beatFile),
|
|
215
|
+
heartbeat_age_sec: heartbeatAge(o.beatFile),
|
|
224
216
|
cmd_consumed: cmdConsumed,
|
|
225
|
-
timeout_sec: timeoutSec,
|
|
217
|
+
timeout_sec: o.timeoutSec,
|
|
226
218
|
sent: payload
|
|
227
219
|
};
|
|
228
|
-
if (diag) {
|
|
220
|
+
if (o.diag) {
|
|
229
221
|
extra.diagnostic = cmdConsumed
|
|
230
222
|
? 'EA komutu aldı ama ack yazmadı (shot işleyicisi hata verdi / PNG üretimi bloke)'
|
|
231
223
|
: 'EA komutu hiç almadı (zamanlayıcı durmuş ya da komut dosyası okunamadı)';
|
|
232
|
-
extra.ping = eaPing(filesDir, 6000);
|
|
224
|
+
extra.ping = eaPing(o.filesDir, 6000);
|
|
233
225
|
}
|
|
234
|
-
return
|
|
235
|
-
(extra.heartbeat_age_sec === null ? '?' : extra.heartbeat_age_sec) + ' sn, cmd tüketildi=' + cmdConsumed + ')', extra
|
|
226
|
+
return { ok: false, err: 'ea_no_ack', error: 'EA ' + o.timeoutSec + ' sn içinde ack vermedi (id=' + o.id + ', heartbeat ' +
|
|
227
|
+
(extra.heartbeat_age_sec === null ? '?' : extra.heartbeat_age_sec) + ' sn, cmd tüketildi=' + cmdConsumed + ')', extra };
|
|
236
228
|
}
|
|
237
229
|
|
|
238
230
|
if (ack.ok === false) {
|
|
239
|
-
return
|
|
231
|
+
return { ok: false, err: 'ea_error', error: 'EA hata döndürdü: ' + (ack.error || 'bilinmeyen'), extra: { ack } };
|
|
240
232
|
}
|
|
241
233
|
|
|
242
234
|
/* --- PNG dosyasını bekle --- */
|
|
243
|
-
const shotFile = (ack.result && ack.result.file) ? path.basename(String(ack.result.file)) : file;
|
|
244
|
-
const src = path.join(filesDir, shotFile);
|
|
235
|
+
const shotFile = (ack.result && ack.result.file) ? path.basename(String(ack.result.file)) : o.params.file;
|
|
236
|
+
const src = path.join(o.filesDir, shotFile);
|
|
245
237
|
let size = 0;
|
|
246
238
|
const pngDeadline = Math.max(ackDeadline, Date.now() + 8000);
|
|
247
239
|
for (;;) {
|
|
@@ -250,12 +242,14 @@ function run() {
|
|
|
250
242
|
syncSleep(250);
|
|
251
243
|
}
|
|
252
244
|
if (size <= 1500) {
|
|
253
|
-
return
|
|
254
|
-
{ ack, expected_file: shotFile, bytes: size });
|
|
245
|
+
return { ok: false, err: 'png_yok', error: 'PNG oluşmadı ya da çok küçük (' + size + ' byte): ' + shotFile, extra: { ack, expected_file: shotFile, bytes: size } };
|
|
255
246
|
}
|
|
247
|
+
return { ok: true, src, shotFile, size, res: ack.result || {}, params: o.params, id: o.id };
|
|
248
|
+
}
|
|
256
249
|
|
|
257
|
-
|
|
258
|
-
|
|
250
|
+
/* --- kopyala: Beast altı yedek + send_file edilebilir kullanıcı klasörü --- */
|
|
251
|
+
function copyOut(src, shotFile, outdir) {
|
|
252
|
+
const pubDir = (outdir && String(outdir).trim()) ? path.resolve(String(outdir).trim()) : PUB_DIR_DEFAULT;
|
|
259
253
|
let pubPath = null, beastPath = null, pubErr = null;
|
|
260
254
|
try {
|
|
261
255
|
if (!fs.existsSync(SHOT_DIR)) fs.mkdirSync(SHOT_DIR, { recursive: true });
|
|
@@ -267,30 +261,176 @@ function run() {
|
|
|
267
261
|
pubPath = path.join(pubDir, shotFile);
|
|
268
262
|
fs.copyFileSync(src, pubPath);
|
|
269
263
|
} catch (e) { pubErr = e.message; }
|
|
264
|
+
return { pubPath, beastPath, pubErr };
|
|
265
|
+
}
|
|
270
266
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const wantSym = params.symbol || res.symbol || null;
|
|
274
|
-
const mismatch = !!(params.symbol && res.symbol && String(res.symbol).toUpperCase() !== String(params.symbol).toUpperCase());
|
|
275
|
-
|
|
276
|
-
/* --- GÖRSELİ ÇAĞIRAN AJANA GÖSTER: PNG data URL olarak sonuca gömülür;
|
|
277
|
-
engine bunu __injectImage sözleşmesiyle sonraki turda vision mesajı yapar
|
|
278
|
-
(ajan grafiği GERÇEKTEN görür). embed:false ile kapatılır; ~3.5MB üstü
|
|
279
|
-
bağlam/dekont şişmesin diye gömülmez (path yine döner). --- */
|
|
267
|
+
/* --- PNG data URL (engine __injectImage sözleşmesiyle ajana görsel enjekte eder) --- */
|
|
268
|
+
function embedData(src, size) {
|
|
280
269
|
let dataUrl = null;
|
|
281
270
|
try {
|
|
282
271
|
if (ARGS.embed !== false && size > 0 && size <= 3500000) {
|
|
283
272
|
dataUrl = 'data:image/png;base64,' + fs.readFileSync(src).toString('base64');
|
|
284
273
|
}
|
|
285
274
|
} catch (e) {}
|
|
275
|
+
return dataUrl;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* ---- ÇOKLU PERİYOT: aynı sembolün N zaman dilimini TEK PNG'de birleştir ----
|
|
279
|
+
Paneller sırayla çekilir (kilit zaten alınmış — ack çakışması yok), sol üste
|
|
280
|
+
"SEMBOL · PERİYOT" etiketi çizilir; soldan sağa (layout:"v" ile üstten alta). */
|
|
281
|
+
async function runMulti(tfList, filesDir, cmdFile, ackFile, beatFile, timeoutSec, diag) {
|
|
282
|
+
let compose = null;
|
|
283
|
+
try { compose = require('./stitch.js').compose; } catch (e) {}
|
|
284
|
+
if (!compose) return { err: 'stitch_fail', error: 'stitch.js bulunamadı (mt5_shot güncel kurulum gerekiyor)' };
|
|
285
|
+
|
|
286
|
+
const n = tfList.length;
|
|
287
|
+
const baseId = 'S' + Date.now().toString(36);
|
|
288
|
+
const fileArg = ARGS.file && String(ARGS.file).trim() ? String(ARGS.file).trim() : '';
|
|
289
|
+
const base = fileArg ? fileArg.replace(/\.[a-z0-9]+$/i, '') : 'beast_mtf_' + baseId;
|
|
290
|
+
const perAck = Math.max(5, Math.min(15, Math.floor(45 / n)));
|
|
291
|
+
const width = Number(ARGS.width) > 0 ? Math.min(10000, Math.round(Number(ARGS.width))) : 1600;
|
|
292
|
+
const height = Number(ARGS.height) > 0 ? Math.min(10000, Math.round(Number(ARGS.height))) : 900;
|
|
293
|
+
const layout = String(ARGS.layout || 'h').toLowerCase() === 'v' ? 'v' : 'h';
|
|
294
|
+
|
|
295
|
+
const panels = [];
|
|
296
|
+
const cmdIds = [];
|
|
297
|
+
let symbol = ARGS.symbol ? String(ARGS.symbol).toUpperCase() : null;
|
|
298
|
+
for (let i = 0; i < n; i++) {
|
|
299
|
+
const tf = tfList[i];
|
|
300
|
+
const id = baseId + 'p' + (i + 1);
|
|
301
|
+
const params = { file: sanitizeFile(base + '_' + tf, 'beast_shot_' + id), width, height, timeframe: tf };
|
|
302
|
+
if (symbol) params.symbol = symbol;
|
|
303
|
+
if (ARGS.template !== undefined) params.template = String(ARGS.template);
|
|
304
|
+
cmdIds.push(id);
|
|
305
|
+
const r = captureOne({ id, file: params.file, params, filesDir, cmdFile, ackFile, beatFile, timeoutSec: perAck, diag });
|
|
306
|
+
if (!r.ok) {
|
|
307
|
+
return {
|
|
308
|
+
err: r.err,
|
|
309
|
+
error: '[' + tf + '] ' + r.error,
|
|
310
|
+
extra: Object.assign({ panel: tf, cmdId: id, requested: { symbol: params.symbol || null, timeframe: tf } }, r.extra || {})
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const cp = copyOut(r.src, r.shotFile, ARGS.outdir);
|
|
314
|
+
panels.push({ tf, src: r.src, file: r.shotFile, size: r.size, res: r.res, pubPath: cp.pubPath, beastPath: cp.beastPath });
|
|
315
|
+
if (!symbol) symbol = (r.res && r.res.symbol) ? String(r.res.symbol) : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* birleştir */
|
|
319
|
+
let composed = null;
|
|
320
|
+
try {
|
|
321
|
+
composed = await compose(
|
|
322
|
+
panels.map((p) => ({ path: p.src, label: (symbol ? symbol + ' · ' : '') + p.tf })),
|
|
323
|
+
{ layout }
|
|
324
|
+
);
|
|
325
|
+
} catch (e) { composed = { ok: false, error: String((e && e.message) || e) }; }
|
|
326
|
+
if (!composed || !composed.ok) {
|
|
327
|
+
return { err: 'stitch_fail', error: 'görüntüler birleştirilemedi: ' + ((composed && composed.error) || 'bilinmeyen') };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const outFile = sanitizeFile(fileArg, 'beast_mtf_' + baseId + '.png');
|
|
331
|
+
const pathMt5 = path.join(filesDir, outFile);
|
|
332
|
+
try { fs.writeFileSync(pathMt5, composed.buffer); }
|
|
333
|
+
catch (e) { return { err: 'stitch_write_fail', error: 'birleşik PNG yazılamadı: ' + e.message }; }
|
|
334
|
+
const size = composed.buffer.length;
|
|
335
|
+
const cp = copyOut(pathMt5, outFile, ARGS.outdir);
|
|
336
|
+
const dataUrl = embedData(pathMt5, size);
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
ok: true,
|
|
340
|
+
payload: {
|
|
341
|
+
ok: true,
|
|
342
|
+
path: cp.pubPath || cp.beastPath || pathMt5, // send_file'a verilecek yol (birleşik)
|
|
343
|
+
path_mt5: pathMt5,
|
|
344
|
+
path_beast: cp.beastPath,
|
|
345
|
+
file: outFile,
|
|
346
|
+
bytes: size,
|
|
347
|
+
image_embedded: !!dataUrl,
|
|
348
|
+
...(dataUrl ? { __injectImage: dataUrl } : {}),
|
|
349
|
+
cmdIds,
|
|
350
|
+
filesDir,
|
|
351
|
+
symbol: symbol || null,
|
|
352
|
+
timeframes: tfList,
|
|
353
|
+
layout,
|
|
354
|
+
panels: panels.map((p, i) => ({
|
|
355
|
+
timeframe: p.tf,
|
|
356
|
+
position: layout === 'h'
|
|
357
|
+
? (i === 0 ? 'left' : (i === n - 1 ? 'right' : 'middle'))
|
|
358
|
+
: (i === 0 ? 'top' : (i === n - 1 ? 'bottom' : 'middle')),
|
|
359
|
+
file: p.file,
|
|
360
|
+
path: p.pubPath || p.beastPath || p.src,
|
|
361
|
+
bytes: p.size,
|
|
362
|
+
period: (p.res && p.res.period) || null
|
|
363
|
+
})),
|
|
364
|
+
width: composed.width,
|
|
365
|
+
height: composed.height,
|
|
366
|
+
requested: { symbol: ARGS.symbol ? String(ARGS.symbol).toUpperCase() : null, timeframes: tfList, width, height, layout },
|
|
367
|
+
heartbeat_age_sec: heartbeatAge(beatFile),
|
|
368
|
+
ms: Date.now() - CTX.started,
|
|
369
|
+
pubErr: cp.pubErr || undefined
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function run() {
|
|
375
|
+
const timeoutSec = Number(ARGS.timeoutSec) > 0
|
|
376
|
+
? Math.max(5, Math.min(70, Math.round(Number(ARGS.timeoutSec))))
|
|
377
|
+
: DEFAULT_TIMEOUT_SEC;
|
|
378
|
+
const diag = ARGS.diag !== false;
|
|
379
|
+
|
|
380
|
+
const term = findTerminalData();
|
|
381
|
+
if (!term) return fail('no_terminal', 'MT5 terminal veri klasörü bulunamadı (beast_ea.json yok) — MT5 kurulu/çalışıyor mu?');
|
|
382
|
+
const filesDir = term.files;
|
|
383
|
+
CTX.filesDir = filesDir;
|
|
384
|
+
|
|
385
|
+
const cmdFile = path.join(filesDir, 'beast_cmd.json');
|
|
386
|
+
const ackFile = path.join(filesDir, 'beast_cmd_ack.json');
|
|
387
|
+
const beatFile = path.join(filesDir, 'beast_ea.json');
|
|
388
|
+
const beatAge = heartbeatAge(beatFile);
|
|
389
|
+
if (beatAge !== null && beatAge > 60) {
|
|
390
|
+
return fail('ea_offline', 'BeastFinance EA canlı değil (son heartbeat ' + beatAge + ' sn önce) — grafikte EA yüklü ve terminal açık mı?',
|
|
391
|
+
{ heartbeat_age_sec: beatAge });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const locked = acquireLock(Math.min(15000, timeoutSec * 500));
|
|
395
|
+
if (!locked) console.error('[mt5_shot] uyarı: kilit alınamadı, yine de devam ediliyor');
|
|
396
|
+
|
|
397
|
+
/* ÇOKLU PERİYOT: timeframes:["M1","M15"] → tek karede yan yana */
|
|
398
|
+
const tfList = normalizeTfs(ARGS);
|
|
399
|
+
if (tfList.length >= 2) {
|
|
400
|
+
const r = await runMulti(tfList, filesDir, cmdFile, ackFile, beatFile, timeoutSec, diag);
|
|
401
|
+
if (!r.ok) return fail(r.err, r.error, r.extra);
|
|
402
|
+
return out(r.payload);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const id = 'S' + Date.now().toString(36);
|
|
406
|
+
CTX.cmdId = id;
|
|
407
|
+
|
|
408
|
+
const file = sanitizeFile(ARGS.file, 'beast_shot_' + id + '.png');
|
|
409
|
+
|
|
410
|
+
const params = { file };
|
|
411
|
+
if (ARGS.timeframe) params.timeframe = String(ARGS.timeframe).toUpperCase();
|
|
412
|
+
if (ARGS.symbol) params.symbol = String(ARGS.symbol).toUpperCase();
|
|
413
|
+
if (ARGS.template !== undefined) params.template = String(ARGS.template);
|
|
414
|
+
params.width = Number(ARGS.width) > 0 ? Math.min(10000, Math.round(Number(ARGS.width))) : 1600;
|
|
415
|
+
params.height = Number(ARGS.height) > 0 ? Math.min(10000, Math.round(Number(ARGS.height))) : 900;
|
|
416
|
+
|
|
417
|
+
const cap = captureOne({ id, file, params, filesDir, cmdFile, ackFile, beatFile, timeoutSec, diag });
|
|
418
|
+
if (!cap.ok) return fail(cap.err, cap.error, cap.extra);
|
|
419
|
+
|
|
420
|
+
const cp = copyOut(cap.src, cap.shotFile, ARGS.outdir);
|
|
421
|
+
const res = cap.res;
|
|
422
|
+
const tfTxt = (res.period ? tfName(res.period) : null) || params.timeframe || null;
|
|
423
|
+
const wantSym = params.symbol || res.symbol || null;
|
|
424
|
+
const mismatch = !!(params.symbol && res.symbol && String(res.symbol).toUpperCase() !== String(params.symbol).toUpperCase());
|
|
425
|
+
const dataUrl = embedData(cap.src, cap.size);
|
|
286
426
|
|
|
287
427
|
return out({
|
|
288
428
|
ok: true,
|
|
289
|
-
path: pubPath || beastPath || src,
|
|
290
|
-
path_mt5: src,
|
|
291
|
-
path_beast: beastPath,
|
|
292
|
-
file: shotFile,
|
|
293
|
-
bytes: size,
|
|
429
|
+
path: cp.pubPath || cp.beastPath || cap.src, // send_file'a verilecek yol
|
|
430
|
+
path_mt5: cap.src,
|
|
431
|
+
path_beast: cp.beastPath,
|
|
432
|
+
file: cap.shotFile,
|
|
433
|
+
bytes: cap.size,
|
|
294
434
|
image_embedded: !!dataUrl,
|
|
295
435
|
...(dataUrl ? { __injectImage: dataUrl } : {}),
|
|
296
436
|
cmdId: id,
|
|
@@ -306,7 +446,7 @@ function run() {
|
|
|
306
446
|
symbol_mismatch: mismatch,
|
|
307
447
|
heartbeat_age_sec: heartbeatAge(beatFile),
|
|
308
448
|
ms: Date.now() - CTX.started,
|
|
309
|
-
pubErr: pubErr || undefined
|
|
449
|
+
pubErr: cp.pubErr || undefined
|
|
310
450
|
});
|
|
311
451
|
}
|
|
312
452
|
|
|
@@ -402,8 +542,9 @@ let raw = '', resolved = false, silenceTimer = null;
|
|
|
402
542
|
function begin(resolve) { if (resolved) return; resolved = true; resolve(); }
|
|
403
543
|
function finish() {
|
|
404
544
|
ARGS = loadArgs(raw);
|
|
405
|
-
|
|
406
|
-
|
|
545
|
+
Promise.resolve()
|
|
546
|
+
.then(run)
|
|
547
|
+
.catch((e) => fail('timeout', 'beklenmeyen hata: ' + (e && e.message ? e.message : String(e))));
|
|
407
548
|
}
|
|
408
549
|
try {
|
|
409
550
|
process.stdin.setEncoding('utf8');
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* mt5_shot yardımcı: birden çok PNG'yi TEK karede birleştirir (yan yana / alt alta).
|
|
3
|
+
- @napi-rs/canvas uygulamanın node_modules'ından yüklenir; kişisel tool çocuk
|
|
4
|
+
prosesi uygulama klasörü dışında koştuğu için yol BEAST_APP_DIR'den gelir
|
|
5
|
+
(customtools env olarak verir; app.asar → app.asar.unpacked otomatik denenir).
|
|
6
|
+
- Panel başına üst köşeye "SEMBOL · PERİYOT" etiketi çizilir: görsel ajan hangi
|
|
7
|
+
tarafın hangi zaman dilimi olduğunu görüntüden de okur.
|
|
8
|
+
- Toplam boyut sınırı aşılırsa paneller eşit oranda küçültülür. */
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
let cached = undefined;
|
|
13
|
+
function loadCanvas() {
|
|
14
|
+
if (cached !== undefined) return cached || null;
|
|
15
|
+
cached = null;
|
|
16
|
+
const cands = [];
|
|
17
|
+
const push = (base) => {
|
|
18
|
+
const b = String(base || '').trim();
|
|
19
|
+
if (!b) return;
|
|
20
|
+
if (!cands.includes(b)) cands.push(b);
|
|
21
|
+
const un = b.replace(/app\.asar([\\/])/i, 'app.asar.unpacked$1');
|
|
22
|
+
if (un !== b && !cands.includes(un)) cands.push(un);
|
|
23
|
+
};
|
|
24
|
+
push(process.env.BEAST_APP_DIR);
|
|
25
|
+
push(path.join(process.env.BEAST_APP_DIR || '', '..'));
|
|
26
|
+
for (const base of cands) {
|
|
27
|
+
try {
|
|
28
|
+
cached = require(path.join(base, 'node_modules', '@napi-rs', 'canvas'));
|
|
29
|
+
if (cached && typeof cached.createCanvas === 'function') break;
|
|
30
|
+
} catch (e) { cached = null; }
|
|
31
|
+
}
|
|
32
|
+
return cached || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const MAX_W = 4800; /* birleşik tuval genişlik tavanı */
|
|
36
|
+
const MAX_H = 3000; /* yükseklik tavanı */
|
|
37
|
+
const MAX_PX = 9 * 1e6; /* toplam piksel tavanı (güvenlik) */
|
|
38
|
+
const GAP = 8; /* paneller arası ayırıcı kalınlığı */
|
|
39
|
+
const LABEL_H = 30; /* etiket şeridi yüksekliği */
|
|
40
|
+
|
|
41
|
+
function pickFont(size) {
|
|
42
|
+
return 'bold ' + size + 'px "Segoe UI", Arial, sans-serif';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* panels: [{ path, label }] · opts: { layout: 'h'|'v' }
|
|
46
|
+
dönüş: { ok:true, buffer, width, height } | { ok:false, error } */
|
|
47
|
+
async function compose(panels, opts) {
|
|
48
|
+
const cv = loadCanvas();
|
|
49
|
+
if (!cv) return { ok: false, error: 'görüntü kütüphanesi bulunamadı (@napi-rs/canvas) — tek periyot çekimi kullanın' };
|
|
50
|
+
const { createCanvas, loadImage } = cv;
|
|
51
|
+
const list = (panels || []).filter((p) => p && p.path);
|
|
52
|
+
if (list.length < 2) return { ok: false, error: 'birleştirme için en az 2 panel gerekli' };
|
|
53
|
+
|
|
54
|
+
const imgs = [];
|
|
55
|
+
for (const p of list) {
|
|
56
|
+
const im = await loadImage(p.path);
|
|
57
|
+
imgs.push({ img: im, label: String(p.label || ''), w: im.width, h: im.height });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const layout = String((opts && opts.layout) || 'h').toLowerCase() === 'v' ? 'v' : 'h';
|
|
61
|
+
let cellW = 0, cellH = 0;
|
|
62
|
+
if (layout === 'h') {
|
|
63
|
+
cellW = Math.max(...imgs.map((i) => i.w));
|
|
64
|
+
cellH = Math.max(...imgs.map((i) => i.h));
|
|
65
|
+
} else {
|
|
66
|
+
cellW = Math.max(...imgs.map((i) => i.w));
|
|
67
|
+
cellH = Math.max(...imgs.map((i) => i.h));
|
|
68
|
+
}
|
|
69
|
+
const n = imgs.length;
|
|
70
|
+
let W = layout === 'h' ? cellW * n + GAP * (n - 1) : cellW;
|
|
71
|
+
let H = layout === 'h' ? cellH : cellH * n + GAP * (n - 1);
|
|
72
|
+
|
|
73
|
+
/* boyut tavanları: eşit oranda küçült */
|
|
74
|
+
let scale = 1;
|
|
75
|
+
if (W > MAX_W) scale = Math.min(scale, MAX_W / W);
|
|
76
|
+
if (H > MAX_H) scale = Math.min(scale, MAX_H / H);
|
|
77
|
+
if (W * H * scale * scale > MAX_PX) scale = Math.min(scale, Math.sqrt(MAX_PX / (W * H)));
|
|
78
|
+
if (scale < 1) {
|
|
79
|
+
cellW = Math.max(320, Math.floor(cellW * scale));
|
|
80
|
+
cellH = Math.max(200, Math.floor(cellH * scale));
|
|
81
|
+
W = layout === 'h' ? cellW * n + GAP * (n - 1) : cellW;
|
|
82
|
+
H = layout === 'h' ? cellH : cellH * n + GAP * (n - 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const canvas = createCanvas(W, H);
|
|
86
|
+
const ctx = canvas.getContext('2d');
|
|
87
|
+
ctx.fillStyle = '#0b0b0b';
|
|
88
|
+
ctx.fillRect(0, 0, W, H);
|
|
89
|
+
|
|
90
|
+
imgs.forEach((p, i) => {
|
|
91
|
+
const x = layout === 'h' ? i * (cellW + GAP) : 0;
|
|
92
|
+
const y = layout === 'h' ? 0 : i * (cellH + GAP);
|
|
93
|
+
ctx.drawImage(p.img, x, y, cellW, cellH);
|
|
94
|
+
/* ayırıcı çizgi */
|
|
95
|
+
if (i > 0) {
|
|
96
|
+
ctx.fillStyle = '#3a3a3a';
|
|
97
|
+
if (layout === 'h') ctx.fillRect(x - GAP, 0, GAP, H);
|
|
98
|
+
else ctx.fillRect(0, y - GAP, W, GAP);
|
|
99
|
+
}
|
|
100
|
+
/* etiket: sol üstte koyu şerit + parlak metin */
|
|
101
|
+
if (p.label) {
|
|
102
|
+
try {
|
|
103
|
+
ctx.font = pickFont(20);
|
|
104
|
+
const tw = Math.min(cellW - 24, Math.ceil(ctx.measureText(p.label).width) + 20);
|
|
105
|
+
ctx.fillStyle = 'rgba(0,0,0,0.78)';
|
|
106
|
+
ctx.fillRect(x + 6, y + 6, Math.max(60, tw), LABEL_H);
|
|
107
|
+
ctx.fillStyle = '#00e676';
|
|
108
|
+
ctx.textBaseline = 'middle';
|
|
109
|
+
ctx.fillText(p.label, x + 15, y + 6 + LABEL_H / 2 + 1);
|
|
110
|
+
} catch (e) {}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let buffer = null;
|
|
115
|
+
try { buffer = canvas.toBuffer('image/png'); } catch (e) {
|
|
116
|
+
return { ok: false, error: 'PNG kodlanamadı: ' + (e && e.message ? e.message : String(e)) };
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, buffer, width: W, height: H };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function available() {
|
|
122
|
+
return !!loadCanvas();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { compose, available };
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mt5_shot",
|
|
3
|
-
"
|
|
3
|
+
"defaultsVersion": 2,
|
|
4
|
+
"description": "MT5 grafiğinin GERÇEK ekran görüntüsünü alır ve GÖRSELİ ÇAĞIRAN AJANA GÖSTERİR: MT5\\MQL5\\Files\\beast_cmd.json üzerinden BeastFinance EA'ya 'shot' komutu yazar, EA ChartScreenShot ile PNG üretir; PNG hem tool klasörüne hem <kullanıcı>\\beast_shots altına kopyalanır (dönen path send_file'a verilebilir) hem de data URL olarak sonuca gömülür — engine bunu sonraki turda ajana görsel olarak enjekte eder (embed:false kapatır). ÇOKLU ZAMAN DİLİMİ: timeframes:[\"M1\",\"M15\"] verirsen aynı sembolün periyotları SIRAYLA çekilip TEK PNG'de birleştirilir (layout:\"h\" soldan sağa — varsayılan; \"v\" üstten alta; 2-3 periyot) ve her panelin üstüne \"SEMBOL · PERİYOT\" etiketi çizilir — solda/sağda hangi periyot olduğu görüntüden de okunur. Aktif grafik istenen sembol/periyot ile aynıysa ondan çekilir (Beast panosu + seviye çizgileri görünür), değilse EA geçici grafik açar (ChartOpen/ChartSetSymbolPeriod + BeastShot.tpl açık tema). Komut id/ts ile yazılır, ack poll edilir ve PNG MQL5\\Files'ta belirene kadar beklenir; hata halinde gövde şu kodu taşır: err = no_terminal|ea_offline|cmd_write_fail|ea_no_ack|ea_error|png_yok|timeout|stitch_fail (+ cmdId, filesDir, diagnostic, ping). Grafik analizi/raporu ve ekip paylaşımı için birincil screenshot aracıdır.",
|
|
4
5
|
"parameters": {
|
|
5
6
|
"type": "object",
|
|
6
7
|
"properties": {
|
|
7
8
|
"symbol": { "type": "string", "description": "Boş = aktif grafik sembolü (örn GOLD)" },
|
|
8
9
|
"timeframe": { "type": "string", "description": "M1|M5|M15|M30|H1|H4|D1 (boş = aktif periyot)" },
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
10
|
+
"timeframes": { "type": "array", "items": { "type": "string" }, "description": "ÇOKLU PERİYOT TEK GÖRÜNTÜ: örn [\"M1\",\"M15\"] — sırayla çekilip tek PNG'de birleştirilir (2-3 adet; soldan sağa)" },
|
|
11
|
+
"tf2": { "type": "string", "description": "İkinci periyot kısayolu (timeframe ile birlikte): örn timeframe M1 + tf2 M15" },
|
|
12
|
+
"layout": { "type": "string", "description": "Çoklu periyot yerleşimi: h = yan yana (varsayılan), v = alt alta" },
|
|
13
|
+
"width": { "type": "number", "description": "Panel başına görüntü genişliği (varsayılan 1600)" },
|
|
14
|
+
"height": { "type": "number", "description": "Panel başına görüntü yüksekliği (varsayılan 900)" },
|
|
15
|
+
"file": { "type": "string", "description": "Opsiyonel dosya adı (varsayılan beast_shot_<id>.png; çoklu periyotta birleşik dosyanın adı)" },
|
|
16
|
+
"timeoutSec": { "type": "number", "description": "Ack/PNG bekleme süresi sn (varsayılan 20, 5-70; çoklu periyotta panel başına 15 sn tavan)" },
|
|
13
17
|
"outdir": { "type": "string", "description": "PNG'nin kopyalanacağı klasör (varsayılan <kullanıcı>\\beast_shots — send_file buradan gönderebilir)" },
|
|
14
18
|
"diag": { "type": "boolean", "description": "Ack gelmezse teşhis amaçlı ping at (varsayılan true)" },
|
|
15
19
|
"embed": { "type": "boolean", "description": "PNG'yi ajana görsel olarak enjekte et (varsayılan true; false = yalnız dosya yolu döner)" }
|
|
16
20
|
},
|
|
17
21
|
"required": []
|
|
18
22
|
},
|
|
19
|
-
"updatedAt": "2026-09-
|
|
23
|
+
"updatedAt": "2026-09-15T00:00:00.000Z"
|
|
20
24
|
}
|
package/src/agent/engine.js
CHANGED
|
@@ -2010,7 +2010,7 @@ class Engine {
|
|
|
2010
2010
|
(roleBlockFull ? roleBlockFull + '\n' : '') +
|
|
2011
2011
|
(teamLine || '') +
|
|
2012
2012
|
'MT5 ARAÇLARI: mt5_status (bağlantı), mt5_account (hesap), mt5_market (canlı fiyat), mt5_positions (açık pozisyonlar), mt5_orders (bekleyen emirler), mt5_history (kapanan işlemler), mt5_ea (BeastFinance grafik panosu: status/ping/chart/note), mt5_trade (piyasa emri), mt5_close (kapat), mt5_modify (SL/TP), mt5_pending (bekleyen emir), mt5_cancel (emir iptal).\n' +
|
|
2013
|
-
'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), 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' +
|
|
2013
|
+
'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 TEK karede soldan sağa), 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' +
|
|
2014
2014
|
'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' +
|
|
2015
2015
|
'YETKİLERİN (AÇIK — çekinmeden kullan):\n' +
|
|
2016
2016
|
'- skill: kurulu SKILL.md kataloğunu oku ve uygula — tool yazmadan ÖNCE skill("tool-yazma"), MT5 tarafı işlerden ÖNCE skill("mql5") oku ve prosedürüne birebir uy.\n' +
|
|
@@ -2018,7 +2018,7 @@ class Engine {
|
|
|
2018
2018
|
'- MQL5: MT5 tarafında script/gösterge/EA yaz (write_file), metaeditor64.exe /compile ile derle, MQL5\\Files dosya köprüsüyle veriyi Beast\'e taşı; kullanıcıya çalıştırma adımını açıkça söyle.\n' +
|
|
2019
2019
|
'- Yerleşik araçlar: run_command, python_run, read_file/write_file/edit_file, web_search/deep_search, browser_*, computer_look (ekran görüntüsü) — hepsi açık.\n' +
|
|
2020
2020
|
'GRAFİK & GÖRSEL DOĞRULAMA (karar öncesi — vazgeçilmez):\n' +
|
|
2021
|
-
'- tool__mt5_shot: MT5 grafiğinin GERÇEK PNG\'si (BeastFinance EA "shot" komutu). Görsel SONRAKİ TURDA gözüne gelir; aktif grafikte Beast panosu + mt5_ea note ile çizilen seviyeler GÖRÜNÜR. symbol/timeframe ver → EA geçici grafik açıp o sembol/periyottan çeker; dönen path send_file ile kullanıcıya gönderilebilir. Grafik analizinde İLK tercih budur.\n' +
|
|
2021
|
+
'- tool__mt5_shot: MT5 grafiğinin GERÇEK PNG\'si (BeastFinance EA "shot" komutu). Görsel SONRAKİ TURDA gözüne gelir; aktif grafikte Beast panosu + mt5_ea note ile çizilen seviyeler GÖRÜNÜR. symbol/timeframe ver → EA geçici grafik açıp o sembol/periyottan çeker; dönen path send_file ile kullanıcıya gönderilebilir. Grafik analizinde İLK tercih budur. ÇOKLU ZAMAN DİLİMİ: timeframes:["M1","M15"] (2-3 periyot, layout:"v" alt alta) → periyotlar sırayla çekilip TEK PNG\'de birleştirilir ve her panelin üstüne "SEMBOL · PERİYOT" etiketi çizilir; solda/sağda hangisi olduğunu görüntüden OKURSUN — trend uyumu/MTF teyidi için bunu kullan (gerçek kapanışlarla).\n' +
|
|
2022
2022
|
'- mt5_ea action:"note": grafiğe kısa plan metni + yatay seviye çizgileri yazar (destek/direnç/SL/TP) — sonra tool__mt5_shot ile çekip seviyelerin doğru yerde olduğunu GÖRSEL doğrula.\n' +
|
|
2023
2023
|
'- computer_look: tüm masaüstü ekranı (MT5 dışı pencereler dahil); browser_screenshot: web grafikleri (TradingView/Investing) — browser_open ile aç.\n' +
|
|
2024
2024
|
'- Görsel göremiyorsan (metin-model) ocr_read (source:"screen"/"browser") ile grafikteki fiyat/seviyeleri metne çevir.\n' +
|
|
@@ -5379,7 +5379,7 @@ const skills = require('./skills');
|
|
|
5379
5379
|
}
|
|
5380
5380
|
/* KİŞİSEL TOOL: %APPDATA%\beast\tools\ — çocuk node prosesinde izole koşar */
|
|
5381
5381
|
if (String(name).startsWith('tool__')) {
|
|
5382
|
-
return JSON.stringify(await customtools.call(name, args));
|
|
5382
|
+
return JSON.stringify(await customtools.call(name, args, sessionId));
|
|
5383
5383
|
}
|
|
5384
5384
|
if (isBc) {
|
|
5385
5385
|
/* opencode permission akışı (tool/external-directory.ts + her aracın
|
package/src/main.js
CHANGED
|
@@ -9317,9 +9317,9 @@ function finCfg() {
|
|
|
9317
9317
|
if (typeof f.notifyTrades !== 'boolean') f.notifyTrades = true;
|
|
9318
9318
|
if (typeof f.notifyWatchdog !== 'boolean') f.notifyWatchdog = true;
|
|
9319
9319
|
/* KANAL OLAY FİLTRESİ: varsayılan true — WhatsApp/Telegram/Discord'a yalnız
|
|
9320
|
-
emir/işlem olayları (açılış, kapanış, bekleyen emir,
|
|
9321
|
-
alarmı, koruma (BE/trailing/kısmi TP), günlük limit ve
|
|
9322
|
-
panelde kalır. false = tüm olaylar kanallara gider
|
|
9320
|
+
emir/işlem olayları (açılış, kapanış, SL/TP güncelleme, bekleyen emir,
|
|
9321
|
+
iptal) düşer; fiyat alarmı, koruma (BE/trailing/kısmi TP), günlük limit ve
|
|
9322
|
+
rapor bildirimleri panelde kalır. false = tüm olaylar kanallara gider. */
|
|
9323
9323
|
if (typeof f.notifyTradeOnly !== 'boolean') f.notifyTradeOnly = true;
|
|
9324
9324
|
if (!Number.isFinite(Number(f.maxDailyLossPct))) f.maxDailyLossPct = 3;
|
|
9325
9325
|
/* GÜNLÜK LİMİT AKSİYONU: warn = yalnız uyarı; stop = tüm finance ajanlarını
|
|
@@ -9566,7 +9566,7 @@ function financeNotify(text, kind, panel) {
|
|
|
9566
9566
|
gider (emir açılış/kapanış, bekleyen emir, iptal). Alarm/koruma/risk/rapor
|
|
9567
9567
|
bildirimleri yalnız panelde görünür. */
|
|
9568
9568
|
const kindStr = String(kind || '');
|
|
9569
|
-
if (cfg.notifyTradeOnly !== false && !['trade', 'close', 'pending', 'cancel'].includes(kindStr)) return;
|
|
9569
|
+
if (cfg.notifyTradeOnly !== false && !['trade', 'close', 'modify', 'pending', 'cancel'].includes(kindStr)) return;
|
|
9570
9570
|
const body = '💼 *Beast Finance*\n' + line;
|
|
9571
9571
|
const senders = [];
|
|
9572
9572
|
try {
|
|
@@ -10246,54 +10246,79 @@ try { finExcLoad(); } catch {}
|
|
|
10246
10246
|
try { finStatsLoad(); } catch {}
|
|
10247
10247
|
try { finEquityLoad(); } catch {}
|
|
10248
10248
|
|
|
10249
|
+
/* İŞLEM OLAYI (ortak hat): iç mt5_* handler'ları ve kişisel tool__mt5_*
|
|
10250
|
+
araçları buradan geçer — jurnal → panel toast → seçili kanallara bildirim. */
|
|
10251
|
+
function finTradeEvent(kRaw, dataRaw, sidRaw) {
|
|
10252
|
+
const k = String(kRaw || '');
|
|
10253
|
+
const d = dataRaw && typeof dataRaw === 'object' ? dataRaw : {};
|
|
10254
|
+
const sid = String(sidRaw || '');
|
|
10255
|
+
const ai = finAgentInfo(sid);
|
|
10256
|
+
const who = ai.label || '';
|
|
10257
|
+
if (k === 'note') {
|
|
10258
|
+
finJournal({ kind: 'note', sid, agent: who, symbol: d.symbol, note: d.note, ticket: d.ticket || 0, side: d.side || '' });
|
|
10259
|
+
return;
|
|
10260
|
+
}
|
|
10261
|
+
/* SHADOW: gerçek emir yok — karar teziyle günlüğe yazılır (test/ölçüm) */
|
|
10262
|
+
if (k === 'shadow') {
|
|
10263
|
+
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 || '' });
|
|
10264
|
+
const sline = `🧪 SHADOW: ${d.side || d.type || ''} ${d.volume || ''} ${d.symbol || ''} — emir gönderilmedi (karar günlüğe yazıldı)`;
|
|
10265
|
+
financeLog('[shadow] ' + sline);
|
|
10266
|
+
finPush('trade', { line: sline });
|
|
10267
|
+
return;
|
|
10268
|
+
}
|
|
10269
|
+
if (k === 'trade') {
|
|
10270
|
+
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 || '' });
|
|
10271
|
+
} else if (k === 'close') {
|
|
10272
|
+
finJournal({ kind: 'close-req', sid, agent: who, ticket: d.ticket, volume: d.volume });
|
|
10273
|
+
return; /* kesin kapanış K/Z'si watchdog tarafından yazılır (çift bildirim yok) */
|
|
10274
|
+
} else if (k === 'modify') {
|
|
10275
|
+
finJournal({ kind: 'modify', sid, agent: who, ticket: d.ticket, sl: d.sl, tp: d.tp });
|
|
10276
|
+
} else if (k === 'pending') {
|
|
10277
|
+
finJournal({ kind: 'pending', sid, agent: who, symbol: d.symbol, type: d.type, volume: d.volume, reason: d.reason || '' });
|
|
10278
|
+
} else if (k === 'cancel') {
|
|
10279
|
+
finJournal({ kind: 'cancel', sid, agent: who, ticket: d.ticket });
|
|
10280
|
+
}
|
|
10281
|
+
let line = '';
|
|
10282
|
+
if (k === 'trade') line = `İŞLEM AÇILDI: ${d.side} ${d.volume} ${d.symbol}${who ? ' · ' + who : ''}`;
|
|
10283
|
+
else if (k === 'modify') line = `SL/TP GÜNCELLENDİ: ticket ${d.ticket} (SL ${d.sl || 0} / TP ${d.tp || 0})${who ? ' · ' + who : ''}`;
|
|
10284
|
+
else if (k === 'pending') line = `BEKLEYEN EMİR: ${d.type} ${d.volume} ${d.symbol}${who ? ' · ' + who : ''}`;
|
|
10285
|
+
else if (k === 'cancel') line = `EMİR İPTAL: ticket ${d.ticket}`;
|
|
10286
|
+
if (line) {
|
|
10287
|
+
financeLog('[ajan] ' + line);
|
|
10288
|
+
finPush('trade', { line });
|
|
10289
|
+
if (finCfg().notifyTrades !== false && (k === 'trade' || k === 'modify' || k === 'pending' || k === 'cancel')) {
|
|
10290
|
+
financeNotify(line, k, false); /* panel toast'ı yukarıda — kanallara gönder */
|
|
10291
|
+
}
|
|
10292
|
+
if (k === 'trade') finStatsRefresh(true).catch(() => {});
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10295
|
+
|
|
10249
10296
|
/* financetools ayar + bildirim kancası: her olay kalıcı günlüğe + bildirime */
|
|
10250
10297
|
try {
|
|
10251
10298
|
financetools.setConfig(() => finCfg());
|
|
10252
10299
|
financetools.setAlerts(finAlertApi());
|
|
10253
10300
|
financetools.setDiscipline((side, symbol, positions) => finDisciplineError(side, symbol, positions));
|
|
10254
10301
|
financetools.setNotify((entry) => {
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
if (
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
}
|
|
10272
|
-
if (
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
finJournal({ kind: 'close-req', sid, agent: who, ticket: d.ticket, volume: d.volume });
|
|
10276
|
-
return; /* kesin kapanış K/Z'si watchdog tarafından yazılır (çift bildirim yok) */
|
|
10277
|
-
} else if (k === 'modify') {
|
|
10278
|
-
finJournal({ kind: 'modify', sid, agent: who, ticket: d.ticket, sl: d.sl, tp: d.tp });
|
|
10279
|
-
} else if (k === 'pending') {
|
|
10280
|
-
finJournal({ kind: 'pending', sid, agent: who, symbol: d.symbol, type: d.type, volume: d.volume, reason: d.reason || '' });
|
|
10281
|
-
} else if (k === 'cancel') {
|
|
10282
|
-
finJournal({ kind: 'cancel', sid, agent: who, ticket: d.ticket });
|
|
10283
|
-
}
|
|
10284
|
-
let line = '';
|
|
10285
|
-
if (k === 'trade') line = `İŞLEM AÇILDI: ${d.side} ${d.volume} ${d.symbol}${who ? ' · ' + who : ''}`;
|
|
10286
|
-
else if (k === 'modify') line = `SL/TP GÜNCELLENDİ: ticket ${d.ticket} (SL ${d.sl || 0} / TP ${d.tp || 0})${who ? ' · ' + who : ''}`;
|
|
10287
|
-
else if (k === 'pending') line = `BEKLEYEN EMİR: ${d.type} ${d.volume} ${d.symbol}${who ? ' · ' + who : ''}`;
|
|
10288
|
-
else if (k === 'cancel') line = `EMİR İPTAL: ticket ${d.ticket}`;
|
|
10289
|
-
if (line) {
|
|
10290
|
-
financeLog('[ajan] ' + line);
|
|
10291
|
-
finPush('trade', { line });
|
|
10292
|
-
if (finCfg().notifyTrades !== false && (k === 'trade' || k === 'pending' || k === 'cancel')) {
|
|
10293
|
-
financeNotify(line, k, false); /* panel toast'ı yukarıda — kanallara gönder */
|
|
10294
|
-
}
|
|
10295
|
-
if (k === 'trade') finStatsRefresh(true).catch(() => {});
|
|
10296
|
-
}
|
|
10302
|
+
finTradeEvent(entry && entry.kind, entry && entry.data, entry && entry.sid);
|
|
10303
|
+
});
|
|
10304
|
+
/* kişisel MT5 toolları köprüyü doğrudan çağırır (iç mt5_* handler'ı devreye
|
|
10305
|
+
girmez) — başarılı emir/SL-TP/bekleyen/iptal olayları aynı hatta düşer */
|
|
10306
|
+
customtools.setTradeHook((id, args, res, sid) => {
|
|
10307
|
+
if (!res || res.ok !== true) return;
|
|
10308
|
+
const kind = {
|
|
10309
|
+
mt5_emir: 'trade',
|
|
10310
|
+
mt5_sltp: 'modify',
|
|
10311
|
+
mt5_bekleyen: 'pending',
|
|
10312
|
+
mt5_emir_iptal: 'cancel',
|
|
10313
|
+
mt5_kapat: 'close',
|
|
10314
|
+
}[String(id || '')];
|
|
10315
|
+
if (!kind) return;
|
|
10316
|
+
const a = args || {};
|
|
10317
|
+
if (kind === 'trade') finTradeEvent('trade', { symbol: a.symbol, side: a.side, volume: a.volume, sl: a.sl, tp: a.tp, reason: a.reason || a.comment || '', result: res }, sid);
|
|
10318
|
+
else if (kind === 'modify') finTradeEvent('modify', { ticket: a.ticket, sl: a.sl, tp: a.tp }, sid);
|
|
10319
|
+
else if (kind === 'pending') finTradeEvent('pending', { symbol: a.symbol, type: a.type, volume: a.volume, reason: a.reason || '' }, sid);
|
|
10320
|
+
else if (kind === 'cancel') finTradeEvent('cancel', { ticket: a.ticket }, sid);
|
|
10321
|
+
else if (kind === 'close') finTradeEvent('close', { ticket: a.ticket, volume: a.volume || 'all' }, sid);
|
|
10297
10322
|
});
|
|
10298
10323
|
} catch {}
|
|
10299
10324
|
|
package/src/renderer/index.html
CHANGED
|
@@ -258,7 +258,7 @@
|
|
|
258
258
|
</select>
|
|
259
259
|
</div>
|
|
260
260
|
<div class="fin-field fin-check">
|
|
261
|
-
<label title="İşaretliyken kanala YALNIZ emir olayları düşer: açılış, kapanış, bekleyen emir, iptal. Fiyat alarmı, BE/trailing/kısmi TP, günlük limit ve rapor bildirimleri gelmez — onlar panelde kalır">
|
|
261
|
+
<label title="İşaretliyken kanala YALNIZ emir olayları düşer: açılış, kapanış, SL/TP güncelleme, bekleyen emir, iptal. Fiyat alarmı, BE/trailing/kısmi TP, günlük limit ve rapor bildirimleri gelmez — onlar panelde kalır">
|
|
262
262
|
<input id="finNotifyTradeOnly" type="checkbox" checked /> Kanala sadece emir/işlem olayları gelsin (alarm gelmez)
|
|
263
263
|
</label>
|
|
264
264
|
</div>
|
package/src/renderer/renderer.js
CHANGED
|
@@ -5549,6 +5549,30 @@ function toggleDmRail(hide) {
|
|
|
5549
5549
|
|
|
5550
5550
|
/* --- modal: SOHBET (yalnız burada tam döküm) --- */
|
|
5551
5551
|
|
|
5552
|
+
/* Yeni mesaj / geç decode olan görsellerde sohbeti otomatik en alta indir:
|
|
5553
|
+
anında + rAF + kısa gecikmeler + görsel load olayları (görsel sonradan
|
|
5554
|
+
yüklenince içerik uzar; tek seferlik scroll yetmez). */
|
|
5555
|
+
function dmStickBottom(box) {
|
|
5556
|
+
if (!box) return;
|
|
5557
|
+
const toBottom = () => {
|
|
5558
|
+
try {
|
|
5559
|
+
box.scrollTop = box.scrollHeight;
|
|
5560
|
+
const p = box.parentElement;
|
|
5561
|
+
if (p && p.scrollHeight > p.clientHeight) p.scrollTop = p.scrollHeight;
|
|
5562
|
+
} catch {}
|
|
5563
|
+
};
|
|
5564
|
+
toBottom();
|
|
5565
|
+
try { requestAnimationFrame(toBottom); } catch {}
|
|
5566
|
+
setTimeout(toBottom, 60);
|
|
5567
|
+
setTimeout(toBottom, 300);
|
|
5568
|
+
for (const img of box.querySelectorAll('img.dmt-img')) {
|
|
5569
|
+
if (img.dataset.dmStick) continue;
|
|
5570
|
+
img.dataset.dmStick = '1';
|
|
5571
|
+
img.addEventListener('load', toBottom, { once: true });
|
|
5572
|
+
img.addEventListener('error', toBottom, { once: true });
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5552
5576
|
function renderDmModal() {
|
|
5553
5577
|
const list = els.dmDialogList;
|
|
5554
5578
|
if (!list) return;
|
|
@@ -5570,8 +5594,7 @@ function renderDmModal() {
|
|
|
5570
5594
|
if (els.dmBackBtn) els.dmBackBtn.hidden = false;
|
|
5571
5595
|
if (els.dmModalDel) els.dmModalDel.hidden = false;
|
|
5572
5596
|
list.innerHTML = '<div class="dm-thread-msgs">' + msgs.map(dmMsgHtml).join('') + '</div>';
|
|
5573
|
-
|
|
5574
|
-
if (box) box.scrollTop = box.scrollHeight;
|
|
5597
|
+
dmStickBottom(list.querySelector('.dm-thread-msgs'));
|
|
5575
5598
|
return;
|
|
5576
5599
|
}
|
|
5577
5600
|
/* OTURUM LİSTESİ — AKTİF + GEÇMİŞ bölümleri */
|