beast-agent 2.32.0 → 2.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
1
+ 'use strict';
2
+
3
+ /* Beast Finance — teknik gösterge matematiği (saf JS, bağımlılıksız).
4
+ mt5_rates'in döndürdüğü OHLC dizilerinden EMA/SMA/RSI/ATR/MACD/BB/Stoch
5
+ hesaplar. UI veya köprü gerektirmez; birim testleri doğrudan bu modülü çağırır. */
6
+
7
+ function nums(arr) {
8
+ return (Array.isArray(arr) ? arr : []).map((v) => {
9
+ const n = Number(v);
10
+ return isFinite(n) ? n : NaN;
11
+ });
12
+ }
13
+
14
+ function smaSeries(values, period) {
15
+ const v = nums(values);
16
+ const out = new Array(v.length).fill(null);
17
+ const p = Math.max(1, Math.round(Number(period) || 1));
18
+ if (v.length < p) return out;
19
+ let sum = 0;
20
+ for (let i = 0; i < v.length; i++) {
21
+ sum += v[i];
22
+ if (i >= p) sum -= v[i - p];
23
+ if (i >= p - 1) out[i] = sum / p;
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function emaSeries(values, period) {
29
+ const v = nums(values);
30
+ const out = new Array(v.length).fill(null);
31
+ const p = Math.max(1, Math.round(Number(period) || 1));
32
+ if (v.length < p) return out;
33
+ const k = 2 / (p + 1);
34
+ let sum = 0;
35
+ for (let i = 0; i < p; i++) sum += v[i];
36
+ let prev = sum / p;
37
+ out[p - 1] = prev;
38
+ for (let i = p; i < v.length; i++) {
39
+ prev = v[i] * k + prev * (1 - k);
40
+ out[i] = prev;
41
+ }
42
+ return out;
43
+ }
44
+
45
+ /* Wilder RSI — ilk ortalama SMA, sonrası Wilder yumuşatması */
46
+ function rsiSeries(closes, period) {
47
+ const c = nums(closes);
48
+ const p = Math.max(2, Math.round(Number(period) || 14));
49
+ const out = new Array(c.length).fill(null);
50
+ if (c.length <= p) return out;
51
+ let gain = 0;
52
+ let loss = 0;
53
+ for (let i = 1; i <= p; i++) {
54
+ const d = c[i] - c[i - 1];
55
+ if (d >= 0) gain += d;
56
+ else loss -= d;
57
+ }
58
+ let avgGain = gain / p;
59
+ let avgLoss = loss / p;
60
+ out[p] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
61
+ for (let i = p + 1; i < c.length; i++) {
62
+ const d = c[i] - c[i - 1];
63
+ const g = d > 0 ? d : 0;
64
+ const l = d < 0 ? -d : 0;
65
+ avgGain = (avgGain * (p - 1) + g) / p;
66
+ avgLoss = (avgLoss * (p - 1) + l) / p;
67
+ out[i] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
68
+ }
69
+ return out;
70
+ }
71
+
72
+ function trueRanges(highs, lows, closes) {
73
+ const h = nums(highs);
74
+ const l = nums(lows);
75
+ const c = nums(closes);
76
+ const out = new Array(c.length).fill(NaN);
77
+ for (let i = 0; i < c.length; i++) {
78
+ const prev = i > 0 ? c[i - 1] : c[i];
79
+ out[i] = Math.max(h[i] - l[i], Math.abs(h[i] - prev), Math.abs(l[i] - prev));
80
+ }
81
+ return out;
82
+ }
83
+
84
+ /* Wilder ATR */
85
+ function atrSeries(highs, lows, closes, period) {
86
+ const tr = trueRanges(highs, lows, closes);
87
+ const p = Math.max(1, Math.round(Number(period) || 14));
88
+ const out = new Array(tr.length).fill(null);
89
+ if (tr.length < p) return out;
90
+ let sum = 0;
91
+ for (let i = 0; i < p; i++) sum += tr[i];
92
+ let prev = sum / p;
93
+ out[p - 1] = prev;
94
+ for (let i = p; i < tr.length; i++) {
95
+ prev = (prev * (p - 1) + tr[i]) / p;
96
+ out[i] = prev;
97
+ }
98
+ return out;
99
+ }
100
+
101
+ function macdSeries(closes, fast, slow, signalPeriod) {
102
+ const f = Math.max(1, Math.round(Number(fast) || 12));
103
+ const s = Math.max(2, Math.round(Number(slow) || 26));
104
+ const sig = Math.max(1, Math.round(Number(signalPeriod) || 9));
105
+ const emaFast = emaSeries(closes, f);
106
+ const emaSlow = emaSeries(closes, s);
107
+ const macd = emaFast.map((v, i) => (v == null || emaSlow[i] == null ? null : v - emaSlow[i]));
108
+ const start = macd.findIndex((v) => v != null);
109
+ const compact = start >= 0 ? macd.slice(start) : [];
110
+ const signalCompact = emaSeries(compact, sig);
111
+ const signal = new Array(macd.length).fill(null);
112
+ if (start >= 0) {
113
+ for (let i = 0; i < signalCompact.length; i++) signal[start + i] = signalCompact[i];
114
+ }
115
+ const hist = macd.map((v, i) => (v == null || signal[i] == null ? null : v - signal[i]));
116
+ return { macd, signal, hist };
117
+ }
118
+
119
+ function bollingerSeries(closes, period, mult) {
120
+ const c = nums(closes);
121
+ const p = Math.max(2, Math.round(Number(period) || 20));
122
+ const m = Number(mult) > 0 ? Number(mult) : 2;
123
+ const mid = smaSeries(c, p);
124
+ const upper = new Array(c.length).fill(null);
125
+ const lower = new Array(c.length).fill(null);
126
+ for (let i = p - 1; i < c.length; i++) {
127
+ let sum = 0;
128
+ for (let j = i - p + 1; j <= i; j++) sum += Math.pow(c[j] - mid[i], 2);
129
+ const sd = Math.sqrt(sum / p);
130
+ upper[i] = mid[i] + m * sd;
131
+ lower[i] = mid[i] - m * sd;
132
+ }
133
+ return { mid, upper, lower };
134
+ }
135
+
136
+ function stochSeries(highs, lows, closes, kPeriod, kSmooth, dPeriod) {
137
+ const h = nums(highs);
138
+ const l = nums(lows);
139
+ const c = nums(closes);
140
+ const kp = Math.max(1, Math.round(Number(kPeriod) || 14));
141
+ const ks = Math.max(1, Math.round(Number(kSmooth) || 3));
142
+ const dp = Math.max(1, Math.round(Number(dPeriod) || 3));
143
+ const raw = new Array(c.length).fill(null);
144
+ for (let i = kp - 1; i < c.length; i++) {
145
+ let hh = -Infinity;
146
+ let ll = Infinity;
147
+ for (let j = i - kp + 1; j <= i; j++) {
148
+ if (h[j] > hh) hh = h[j];
149
+ if (l[j] < ll) ll = l[j];
150
+ }
151
+ raw[i] = hh === ll ? 50 : ((c[i] - ll) / (hh - ll)) * 100;
152
+ }
153
+ const k = smaSeries(raw, ks);
154
+ const d = smaSeries(k, dp);
155
+ return { k, d };
156
+ }
157
+
158
+ /* "EMA(50)" | "ema50" | "RSI(14)" | "ATR(14)" | "MACD(12,26,9)" |
159
+ "BB(20,2)" | "SMA(200)" | "STOCH(14,3,3)" → {type, params} */
160
+ function parseSpec(spec) {
161
+ const s = String(spec || '').trim().toUpperCase().replace(/\s+/g, '');
162
+ const m = /^([A-Z]+)\(?([0-9,.\s]*)\)?$/.exec(s);
163
+ if (!m) return null;
164
+ const type = m[1];
165
+ const params = (m[2] ? m[2].split(',').map((x) => Number(x)) : []).filter((x) => isFinite(x));
166
+ if (!['EMA', 'SMA', 'RSI', 'ATR', 'MACD', 'BB', 'STOCH'].includes(type)) return null;
167
+ return { type, params };
168
+ }
169
+
170
+ function lastN(series, n) {
171
+ const v = (Array.isArray(series) ? series : []).filter((x) => x != null);
172
+ const size = Math.max(1, Math.min(20, Number(n) || 1));
173
+ return v.slice(-size).map((x) => Math.round(x * 1e6) / 1e6);
174
+ }
175
+
176
+ /* Ham seriyi hesaplar; dönen değerler tam uzunlukta (warmup null). */
177
+ function seriesFor(spec, rates) {
178
+ const closes = rates.map((r) => Number(r.close));
179
+ const highs = rates.map((r) => Number(r.high));
180
+ const lows = rates.map((r) => Number(r.low));
181
+ const p = spec.params;
182
+ switch (spec.type) {
183
+ case 'EMA':
184
+ return { name: 'EMA', params: [p[0] || 20], series: emaSeries(closes, p[0] || 20) };
185
+ case 'SMA':
186
+ return { name: 'SMA', params: [p[0] || 20], series: smaSeries(closes, p[0] || 20) };
187
+ case 'RSI':
188
+ return { name: 'RSI', params: [p[0] || 14], series: rsiSeries(closes, p[0] || 14) };
189
+ case 'ATR':
190
+ return { name: 'ATR', params: [p[0] || 14], series: atrSeries(highs, lows, closes, p[0] || 14) };
191
+ case 'MACD': {
192
+ const r = macdSeries(closes, p[0] || 12, p[1] || 26, p[2] || 9);
193
+ return { name: 'MACD', params: [p[0] || 12, p[1] || 26, p[2] || 9], series: r.macd, extra: { signal: r.signal, hist: r.hist } };
194
+ }
195
+ case 'BB': {
196
+ const r = bollingerSeries(closes, p[0] || 20, p[1] || 2);
197
+ return { name: 'BB', params: [p[0] || 20, p[1] || 2], series: r.mid, extra: { upper: r.upper, lower: r.lower } };
198
+ }
199
+ case 'STOCH': {
200
+ const r = stochSeries(highs, lows, closes, p[0] || 14, p[1] || 3, p[2] || 3);
201
+ return { name: 'STOCH', params: [p[0] || 14, p[1] || 3, p[2] || 3], series: r.k, extra: { d: r.d } };
202
+ }
203
+ default:
204
+ return null;
205
+ }
206
+ }
207
+
208
+ const DEFAULT_SPECS = ['EMA(20)', 'EMA(50)', 'RSI(14)', 'ATR(14)', 'MACD(12,26,9)'];
209
+
210
+ /* rates: [{time, open, high, low, close, ...}] → ajan dostu özet.
211
+ Her gösterge için son 3 değer + (varsa) ek seriler. */
212
+ function compute(rates, specs) {
213
+ const rows = Array.isArray(rates) ? rates : [];
214
+ const list = (Array.isArray(specs) && specs.length ? specs : DEFAULT_SPECS)
215
+ .map((s) => (typeof s === 'string' ? parseSpec(s) : s))
216
+ .filter(Boolean)
217
+ .slice(0, 12);
218
+ const close = rows.length ? Number(rows[rows.length - 1].close) : null;
219
+ const out = { bars: rows.length, close, indicators: {} };
220
+ for (const spec of list) {
221
+ const r = seriesFor(spec, rows);
222
+ if (!r) continue;
223
+ const key = r.name + '(' + r.params.join(',') + ')';
224
+ const entry = { last: lastN(r.series, 3) };
225
+ if (r.extra) {
226
+ entry.extra = {};
227
+ for (const [k, v] of Object.entries(r.extra)) entry.extra[k] = lastN(v, 3);
228
+ }
229
+ out.indicators[key] = entry;
230
+ }
231
+ const highs = rows.map((x) => Number(x.high));
232
+ const lows = rows.map((x) => Number(x.low));
233
+ if (highs.length) {
234
+ const window = Math.min(20, rows.length);
235
+ out.range20 = {
236
+ high: Math.max(...highs.slice(-window)),
237
+ low: Math.min(...lows.slice(-window)),
238
+ };
239
+ }
240
+ return out;
241
+ }
242
+
243
+ module.exports = {
244
+ parseSpec,
245
+ compute,
246
+ smaSeries,
247
+ emaSeries,
248
+ rsiSeries,
249
+ atrSeries,
250
+ macdSeries,
251
+ bollingerSeries,
252
+ stochSeries,
253
+ DEFAULT_SPECS,
254
+ };
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ /* Beast Finance — işlem öncesi risk katmanı (saf; köprü/IO yok).
4
+ Lot normalizasyonu (broker min/max/step), %risk ile lot hesabı (SL mesafesi
5
+ veya ATR), stops_level doğrulaması, marj kalkanı ve yoğunluk (net yön)
6
+ kontrolü. financetools bu kararları uygular; birim testleri doğrudan çağırır. */
7
+
8
+ function num(v, def) {
9
+ const n = Number(v);
10
+ return isFinite(n) ? n : def;
11
+ }
12
+
13
+ function stepFloor(v, step) {
14
+ const s = num(step, 0.01);
15
+ if (!(s > 0)) return v;
16
+ return Math.floor(v / s + 1e-9) * s;
17
+ }
18
+
19
+ /* info: MT5 symbol_info satırı. wanted: istenen lot. hardMax: kullanıcı limiti. */
20
+ function normalizeVolume(info, wanted, hardMax) {
21
+ const vmin = num(info && info.volume_min, 0.01);
22
+ const vmax = num(info && info.volume_max, 100);
23
+ const step = num(info && info.volume_step, 0.01);
24
+ let v = num(wanted, 0);
25
+ if (!(v > 0)) return { error: 'volume gerekli (pozitif sayı)' };
26
+ const cap = Math.min(vmax > 0 ? vmax : Infinity, num(hardMax, 0) > 0 ? num(hardMax, 0) : Infinity);
27
+ let capped = false;
28
+ if (v > cap) {
29
+ v = stepFloor(cap, step);
30
+ capped = true;
31
+ }
32
+ v = Math.round(stepFloor(v, step) * 1e8) / 1e8;
33
+ if (!(v > 0) || v < vmin - 1e-9) {
34
+ return { error: `hacim broker minimumunun altında (min ${vmin} lot)` };
35
+ }
36
+ return { volume: v, capped };
37
+ }
38
+
39
+ /* 1 lot için SL mesafesi kaybı (hesap para birimi): tick değeri, yoksa kontrat */
40
+ function lossPerLot(info, entry, sl) {
41
+ const dist = Math.abs(num(entry, 0) - num(sl, 0));
42
+ if (!(dist > 0)) return 0;
43
+ const tickSize = num(info && info.trade_tick_size, 0) || num(info && info.point, 0);
44
+ const tickValue = num(info && info.trade_tick_value, 0);
45
+ if (tickSize > 0 && tickValue > 0) return (dist / tickSize) * tickValue;
46
+ const contract = num(info && info.trade_contract_size, 0);
47
+ if (contract > 0) return dist * contract;
48
+ return 0;
49
+ }
50
+
51
+ /* riskAmount: hesap para birimi cinsinden kaybedilecek tutar */
52
+ function calcRiskLot(info, entry, sl, riskAmount, hardMax) {
53
+ const lpl = lossPerLot(info, entry, sl);
54
+ if (!(lpl > 0)) return { error: 'risk hesaplanamadı (tick değeri/kontrat bilgisi yok)' };
55
+ const raw = num(riskAmount, 0) / lpl;
56
+ const norm = normalizeVolume(info, raw, hardMax);
57
+ if (norm.error) return { error: norm.error, raw, lossPerLot: lpl };
58
+ return {
59
+ volume: norm.volume,
60
+ raw: Math.round(raw * 1e4) / 1e4,
61
+ lossPerLot: Math.round(lpl * 100) / 100,
62
+ riskAmount: Math.round(raw * lpl * 100) / 100,
63
+ capped: !!norm.capped,
64
+ };
65
+ }
66
+
67
+ /* stops_level ihlali: SL/TP fiyata çok yakın mı? (broker reddini önler)
68
+ Yakınsa Türkçe hata metni, sorun yoksa null döner. */
69
+ function stopsTooClose(info, price, sl, tp) {
70
+ const stops = num(info && info.trade_stops_level, 0);
71
+ if (!(stops > 0)) return null;
72
+ const point = num(info && info.point, 0);
73
+ if (!(point > 0)) return null;
74
+ const minDist = stops * point;
75
+ const p = num(price, 0);
76
+ const s = num(sl, 0);
77
+ const t = num(tp, 0);
78
+ if (p > 0 && s > 0 && Math.abs(p - s) < minDist - 1e-12) {
79
+ return `SL fiyata çok yakın — broker en az ${minDist.toFixed(point < 0.01 ? 3 : 2)} mesafe istiyor`;
80
+ }
81
+ if (p > 0 && t > 0 && Math.abs(p - t) < minDist - 1e-12) {
82
+ return `TP fiyata çok yakın — broker en az ${minDist.toFixed(point < 0.01 ? 3 : 2)} mesafe istiyor`;
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /* Marj kalkanı: mevcut serbest marj + işlem sonrası tahmini marj seviyesi.
88
+ Sorun varsa hata metni, yoksa null. */
89
+ function marginCheck(account, estMargin, minLevel) {
90
+ const equity = num(account && account.equity, 0);
91
+ const used = num(account && account.margin, 0);
92
+ const free = num(account && account.margin_free, 0);
93
+ const current = num(account && account.margin_level, 0);
94
+ const m = num(estMargin, 0);
95
+ const minL = num(minLevel, 0);
96
+ if (m > 0 && free > 0 && m > free) {
97
+ return { error: `yetersiz serbest marj (gerekli ~${m.toFixed(2)}, serbest ${free.toFixed(2)})` };
98
+ }
99
+ if (minL > 0 && current > 0 && current < minL) {
100
+ return { error: `marj seviyesi kalkanı: mevcut %${current.toFixed(0)} < min %${minL}` };
101
+ }
102
+ if (minL > 0 && equity > 0 && (used > 0 || m > 0)) {
103
+ const after = (equity / (used + m)) * 100;
104
+ if (after < minL) {
105
+ return { error: `marj seviyesi kalkanı: işlem sonrası tahmini %${after.toFixed(0)} < min %${minL}`, level: Math.round(after * 100) / 100 };
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ /* Yoğunluk: aynı yöne birikme + sembol başına aynı yön limiti.
112
+ Sorun varsa hata metni, yoksa null. 0 = ilgili kural kapalı. */
113
+ function exposureCheck(positions, side, symbol, maxPerSymbol, maxSameSide) {
114
+ const list = Array.isArray(positions) ? positions : [];
115
+ const isBuy = String(side).toLowerCase() === 'buy';
116
+ const sym = String(symbol || '').toUpperCase();
117
+ const maxSame = num(maxSameSide, 0);
118
+ const maxSym = num(maxPerSymbol, 0);
119
+ if (maxSame > 0) {
120
+ const sameDir = list.filter((p) => (Number(p.type) === 0) === isBuy).length;
121
+ if (sameDir >= maxSame) return `aynı yönde ${sameDir} pozisyon var (max ${maxSame}) — çeşitlendir`;
122
+ }
123
+ if (maxSym > 0) {
124
+ const sameSym = list.filter(
125
+ (p) => String(p.symbol || '').toUpperCase() === sym && (Number(p.type) === 0) === isBuy
126
+ ).length;
127
+ if (sameSym >= maxSym) return `${sym} ${isBuy ? 'BUY' : 'SELL'} için ${sameSym} pozisyon var (max ${maxSym})`;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ module.exports = { normalizeVolume, lossPerLot, calcRiskLot, stopsTooClose, marginCheck, exposureCheck };
@@ -0,0 +1,229 @@
1
+ 'use strict';
2
+
3
+ /* Beast Finance — muhasebe matematiği (saf; köprü/IO yok).
4
+ MT5 deals listesinden işlem istatistiği, equity serisinden drawdown ve
5
+ haftalık rapor metni üretir. main süreç bu çıktıları dosyaya/panoya taşır. */
6
+
7
+ function num(v, def) {
8
+ const n = Number(v);
9
+ return isFinite(n) ? n : def;
10
+ }
11
+
12
+ function dayKey(ms) {
13
+ const d = new Date(Number(ms) || 0);
14
+ const p = (x) => String(x).padStart(2, '0');
15
+ return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
16
+ }
17
+
18
+ function isOut(entry) {
19
+ return entry === 1 || entry === '1' || String(entry) === '1';
20
+ }
21
+
22
+ function once(v, d) {
23
+ const n = Number(v);
24
+ return isFinite(n) ? n : d;
25
+ }
26
+
27
+ /* deals: [{position_id, entry, profit, swap, commission, symbol, time, volume}] */
28
+ function summarizeDeals(deals, opts) {
29
+ const rows = Array.isArray(deals) ? deals : [];
30
+ const since = opts && Number.isFinite(Number(opts.sinceMs)) ? Number(opts.sinceMs) : 0;
31
+ const groups = new Map();
32
+ for (const dl of rows) {
33
+ const pid = String(dl.position_id != null ? dl.position_id : dl.ticket != null ? dl.ticket : '');
34
+ if (!pid) continue;
35
+ let g = groups.get(pid);
36
+ if (!g) {
37
+ g = { pid, symbol: String(dl.symbol || ''), net: 0, hasOut: false, openTime: Infinity, closeTime: 0, volume: 0 };
38
+ groups.set(pid, g);
39
+ }
40
+ g.net += once(dl.profit, 0) + once(dl.swap, 0) + once(dl.commission, 0);
41
+ const t = once(dl.time, 0) * (once(dl.time, 0) > 1e12 ? 1 : 1000);
42
+ if (t > 0) {
43
+ if (t < g.openTime) g.openTime = t;
44
+ if (t > g.closeTime) g.closeTime = t;
45
+ }
46
+ if (isOut(dl.entry)) g.hasOut = true;
47
+ if (!g.symbol && dl.symbol) g.symbol = String(dl.symbol);
48
+ g.volume = Math.max(g.volume, once(dl.volume, 0));
49
+ }
50
+ const closed = [...groups.values()].filter((g) => g.hasOut && (!since || g.closeTime >= since));
51
+ const total = closed.length;
52
+ let wins = 0;
53
+ let losses = 0;
54
+ let breakeven = 0;
55
+ let grossProfit = 0;
56
+ let grossLoss = 0;
57
+ let best = 0;
58
+ let worst = 0;
59
+ let bestSymbol = '';
60
+ let worstSymbol = '';
61
+ const bySymbol = {};
62
+ const byDay = {};
63
+ let lastTradeAt = 0;
64
+ for (const g of closed) {
65
+ const n = Math.round(g.net * 100) / 100;
66
+ const k = dayKey(g.closeTime);
67
+ byDay[k] = Math.round(((byDay[k] || 0) + n) * 100) / 100;
68
+ if (!bySymbol[g.symbol]) bySymbol[g.symbol] = { net: 0, trades: 0, wins: 0, losses: 0 };
69
+ const s = bySymbol[g.symbol];
70
+ s.net = Math.round((s.net + n) * 100) / 100;
71
+ s.trades++;
72
+ if (n > 0) {
73
+ wins++;
74
+ s.wins++;
75
+ grossProfit += n;
76
+ if (n > best) {
77
+ best = n;
78
+ bestSymbol = g.symbol;
79
+ }
80
+ } else if (n < 0) {
81
+ losses++;
82
+ s.losses++;
83
+ grossLoss += -n;
84
+ if (n < worst) {
85
+ worst = n;
86
+ worstSymbol = g.symbol;
87
+ }
88
+ } else {
89
+ breakeven++;
90
+ }
91
+ if (g.closeTime > lastTradeAt) lastTradeAt = g.closeTime;
92
+ }
93
+ const netProfit = Math.round((grossProfit - grossLoss) * 100) / 100;
94
+ return {
95
+ trades: total,
96
+ wins,
97
+ losses,
98
+ breakeven,
99
+ winRate: total ? Math.round((wins / total) * 1000) / 10 : 0,
100
+ netProfit,
101
+ grossProfit: Math.round(grossProfit * 100) / 100,
102
+ grossLoss: Math.round(grossLoss * 100) / 100,
103
+ profitFactor: grossLoss > 0 ? Math.round((grossProfit / grossLoss) * 100) / 100 : grossProfit > 0 ? null : 0,
104
+ avgWin: wins ? Math.round((grossProfit / wins) * 100) / 100 : 0,
105
+ avgLoss: losses ? Math.round((grossLoss / losses) * 100) / 100 : 0,
106
+ best: Math.round(best * 100) / 100,
107
+ bestSymbol,
108
+ worst: Math.round(worst * 100) / 100,
109
+ worstSymbol,
110
+ bySymbol,
111
+ byDay,
112
+ lastTradeAt: lastTradeAt || 0,
113
+ at: Date.now(),
114
+ };
115
+ }
116
+
117
+ /* points: [{at, equity}] — sırasız gelebilir; zamana göre sıralanır. */
118
+ function maxDrawdown(points) {
119
+ const list = (Array.isArray(points) ? points : [])
120
+ .map((p) => ({ at: num(p && p.at, 0), equity: num(p && p.equity, NaN) }))
121
+ .filter((p) => isFinite(p.equity))
122
+ .sort((a, b) => a.at - b.at);
123
+ let peak = -Infinity;
124
+ let peakAt = 0;
125
+ let maxAbs = 0;
126
+ let maxPct = 0;
127
+ let troughAt = 0;
128
+ for (const p of list) {
129
+ if (p.equity > peak) {
130
+ peak = p.equity;
131
+ peakAt = p.at;
132
+ }
133
+ if (peak > 0) {
134
+ const abs = peak - p.equity;
135
+ const pct = (abs / peak) * 100;
136
+ if (abs > maxAbs) {
137
+ maxAbs = abs;
138
+ maxPct = pct;
139
+ troughAt = p.at;
140
+ }
141
+ }
142
+ }
143
+ return {
144
+ maxAbs: Math.round(maxAbs * 100) / 100,
145
+ maxPct: Math.round(maxPct * 100) / 100,
146
+ peakAt,
147
+ troughAt,
148
+ samples: list.length,
149
+ };
150
+ }
151
+
152
+ function money(v, cur) {
153
+ const n = num(v, 0);
154
+ const sign = n > 0 ? '+' : '';
155
+ return sign + n.toFixed(2) + (cur ? ' ' + cur : '');
156
+ }
157
+
158
+ /* Haftalık rapor metni (markdown). review: ajan eleştirisi (opsiyonel). */
159
+ function buildWeeklyReport(ctx) {
160
+ const c = ctx || {};
161
+ const st = c.stats || {};
162
+ const cur = (c.account && c.account.currency) || '';
163
+ const L = [];
164
+ const from = dayKey(c.fromTs || Date.now() - 7 * 86400000);
165
+ const to = dayKey(c.toTs || Date.now());
166
+ L.push('# Beast Finance — Haftalık Rapor');
167
+ L.push('');
168
+ L.push('Dönem: **' + from + ' → ' + to + '**');
169
+ L.push('');
170
+ if (c.account) {
171
+ L.push('## Hesap');
172
+ L.push('- Bakiye: **' + num(c.account.balance, 0).toFixed(2) + ' ' + cur + '** · Özkaynak: ' + num(c.account.equity, 0).toFixed(2) + ' ' + cur);
173
+ L.push('- Kaldıraç: 1:' + num(c.account.leverage, 0) + ' · Para birimi: ' + (cur || '?'));
174
+ L.push('');
175
+ }
176
+ L.push('## Performans');
177
+ L.push('| Metrik | Değer |');
178
+ L.push('| --- | --- |');
179
+ L.push('| Kapanan işlem | ' + num(st.trades, 0) + ' |');
180
+ L.push('| Kazanç / Kayıp | ' + num(st.wins, 0) + ' / ' + num(st.losses, 0) + ' |');
181
+ L.push('| Win rate | %' + num(st.winRate, 0) + ' |');
182
+ L.push('| Net K/Z | ' + money(st.netProfit, cur) + ' |');
183
+ L.push('| Profit factor | ' + (st.profitFactor == null ? '∞' : st.profitFactor) + ' |');
184
+ if (c.drawdown) L.push('| Max drawdown | ' + num(c.drawdown.maxAbs, 0).toFixed(2) + ' ' + cur + ' (%' + num(c.drawdown.maxPct, 0) + ') |');
185
+ L.push('');
186
+ const days = Object.entries(st.byDay || {}).sort((a, b) => a[0].localeCompare(b[0])).slice(-7);
187
+ if (days.length) {
188
+ L.push('## Günlük K/Z');
189
+ for (const [d, v] of days) L.push('- ' + d + ': ' + money(v, cur));
190
+ L.push('');
191
+ }
192
+ const syms = Object.entries(st.bySymbol || {})
193
+ .map(([s, v]) => ({ symbol: s, ...v }))
194
+ .sort((a, b) => b.net - a.net)
195
+ .slice(0, 8);
196
+ if (syms.length) {
197
+ L.push('## Semboller');
198
+ L.push('| Sembol | İşlem | W/L | Net |');
199
+ L.push('| --- | --- | --- | --- |');
200
+ for (const s of syms) L.push('| ' + s.symbol + ' | ' + s.trades + ' | ' + s.wins + '/' + s.losses + ' | ' + money(s.net, cur) + ' |');
201
+ L.push('');
202
+ }
203
+ if (c.positions && c.positions.length) {
204
+ L.push('## Açık Pozisyonlar');
205
+ for (const p of c.positions.slice(0, 15)) {
206
+ L.push('- ' + (p.symbol || '?') + ' ' + (Number(p.type) === 0 ? 'BUY' : 'SELL') + ' ' + num(p.volume, 0) + ' lot @ ' + num(p.price_open, 0) + ' → K/Z ' + money(p.profit, cur));
207
+ }
208
+ L.push('');
209
+ }
210
+ if (Array.isArray(c.journal) && c.journal.length) {
211
+ L.push('## Son İşlem Günlüğü');
212
+ for (const j of c.journal.slice(-15)) {
213
+ const t = new Date(num(j.at, Date.now()));
214
+ const hh = String(t.getHours()).padStart(2, '0') + ':' + String(t.getMinutes()).padStart(2, '0');
215
+ L.push('- ' + hh + ' · ' + String(j.kind || '') + (j.symbol ? ' ' + j.symbol : '') + (j.side ? ' ' + j.side : '') + (j.volume ? ' ' + j.volume : '') + (j.reason ? ' — ' + String(j.reason).slice(0, 160) : ''));
216
+ }
217
+ L.push('');
218
+ }
219
+ if (c.review) {
220
+ L.push('## Ajan Değerlendirmesi');
221
+ L.push(String(c.review).trim());
222
+ L.push('');
223
+ }
224
+ L.push('---');
225
+ L.push('_Beast Finance tarafından otomatik üretildi · ' + new Date().toISOString() + '_');
226
+ return L.join('\n');
227
+ }
228
+
229
+ module.exports = { summarizeDeals, maxDrawdown, buildWeeklyReport, dayKey, money };