alterzick commited on
Commit
0638967
·
verified ·
1 Parent(s): 768025f

Add 3 files

Browse files
Files changed (3) hide show
  1. README.md +7 -5
  2. index.html +1355 -19
  3. prompts.txt +1 -0
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Broksum V2
3
- emoji: 🐢
4
- colorFrom: red
5
- colorTo: gray
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: broksum-v2
3
+ emoji: ⚛️
4
+ colorFrom: pink
5
+ colorTo: red
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - QwenSite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1355 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Lembar Kerja Ringkasan Saham IDX (XLSX, Multi-Hari)</title>
6
+ <script src="https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js"></script>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <style>
11
+ * { box-sizing: border-box; font-family: Arial, sans-serif; }
12
+ body { margin: 0; padding: 12px; background:#f5f5f5; font-size:12px; }
13
+ h2, h3 { margin-top:0; }
14
+ .panel {
15
+ background:#fff; padding:10px; border-radius:6px; box-shadow:0 1px 4px rgba(0,0,0,0.1);
16
+ margin-bottom:10px; border:1px solid #eee;
17
+ }
18
+ label { font-weight:bold; }
19
+ input[type="file"], select, input[type="text"], input[type="date"], button {
20
+ font-size:12px; padding:2px 4px;
21
+ }
22
+ button {
23
+ cursor:pointer;
24
+ transition: all 0.3s;
25
+ }
26
+ button:hover {
27
+ background: #f0f0f0;
28
+ }
29
+ .row {
30
+ display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:4px;
31
+ }
32
+ .row > div { display:flex; flex-direction:column; }
33
+ .wrap-table { max-height:70vh; overflow:auto; border:1px solid #ccc; border-radius:4px; background:#fff; }
34
+ table { border-collapse:collapse; width:100%; font-size:11px; }
35
+ th, td { border:1px solid #ddd; padding:2px 4px; white-space:nowrap; }
36
+ th {
37
+ background:#f0f0f0; position:sticky; top:0; z-index:1; cursor:pointer;
38
+ user-select: none;
39
+ }
40
+ th:hover {
41
+ background: #e0e0e0;
42
+ }
43
+ tr:hover { background:#fafad2; }
44
+ .up { color:#006400; font-weight:bold; }
45
+ .down { color:#b22222; font-weight:bold; }
46
+ #info { margin-top:4px; color:#555; }
47
+ #datasetInfo { margin-top:4px; color:#333; }
48
+ .sort-ind { font-size:10px; margin-left:2px; }
49
+ .pill {
50
+ display: inline-block;
51
+ padding: 2px 8px;
52
+ border-radius: 12px;
53
+ background: #f0f0f0;
54
+ font-size: 10px;
55
+ margin: 2px;
56
+ }
57
+ .up-pill {
58
+ background: rgba(0,100,0,0.1);
59
+ color: #006400;
60
+ }
61
+ .down-pill {
62
+ background: rgba(178,34,34,0.1);
63
+ color: #b22222;
64
+ }
65
+ .card {
66
+ background: #fff;
67
+ border-radius: 6px;
68
+ padding: 12px;
69
+ box-shadow: 0 1px 4px rgba(0,0,0,0.1);
70
+ margin-bottom: 12px;
71
+ }
72
+ .tab-container {
73
+ display: flex;
74
+ border-bottom: 1px solid #ddd;
75
+ margin-bottom: 12px;
76
+ }
77
+ .tab {
78
+ padding: 8px 16px;
79
+ cursor: pointer;
80
+ border: 1px solid transparent;
81
+ border-bottom: none;
82
+ border-radius: 6px 6px 0 0;
83
+ margin-right: 4px;
84
+ }
85
+ .tab.active {
86
+ border-color: #ddd;
87
+ background: #fff;
88
+ border-bottom: 2px solid #fff;
89
+ margin-bottom: -1px;
90
+ }
91
+ .tab-content {
92
+ display: none;
93
+ }
94
+ .tab-content.active {
95
+ display: block;
96
+ }
97
+ .chart-container {
98
+ position: relative;
99
+ height: 300px;
100
+ margin-bottom: 16px;
101
+ }
102
+ .stats-item {
103
+ padding: 8px;
104
+ border-bottom: 1px dashed #eee;
105
+ }
106
+ .stats-value {
107
+ font-weight: bold;
108
+ color: #333;
109
+ }
110
+ </style>
111
+ </head>
112
+ <body>
113
+ <div class="tab-container">
114
+ <div class="tab active" data-tab="main">Data Utama</div>
115
+ <div class="tab" data-tab="analysis">Analisis</div>
116
+ <div class="tab" data-tab="recommendations">Rekomendasi</div>
117
+ </div>
118
+
119
+ <!-- Main Data Tab -->
120
+ <div id="main" class="tab-content active">
121
+ <div class="panel">
122
+ <h2>Lembar Kerja Ringkasan Saham IDX (XLSX, Multi-Hari)</h2>
123
+ <p class="mb-2">
124
+ <strong>Fitur:</strong> Upload file <b>.xlsx</b> ringkasan saham (sheet pertama). Dataset disimpan per tanggal
125
+ di <code>localStorage</code> sehingga bisa dikumpulkan dan dianalisis ulang. Klik header
126
+ kolom untuk sort ascending/descending.
127
+ </p>
128
+
129
+ <div class="row">
130
+ <div>
131
+ <label for="tradeDate">Tanggal Perdagangan (YYYY-MM-DD)</label>
132
+ <input type="date" id="tradeDate">
133
+ </div>
134
+
135
+ <div>
136
+ <label for="fileInput">Upload File Ringkasan (.xlsx)</label>
137
+ <input type="file" id="fileInput" accept=".xlsx">
138
+ </div>
139
+
140
+ <div class="mt-4">
141
+ <button id="clearStorage" class="bg-red-100 text-red-800 p-2 rounded">
142
+ <i class="fas fa-trash mr-1"></i>Hapus Semua Data
143
+ </button>
144
+ </div>
145
+ </div>
146
+
147
+ <div class="row" id="filterBar" style="margin-top:10px; display:none;">
148
+ <div>
149
+ <label for="datasetSelect">Dataset (Tanggal)</label>
150
+ <select id="datasetSelect"></select>
151
+ </div>
152
+ <div>
153
+ <label for="filterCode">Filter Code</label>
154
+ <input type="text" id="filterCode" placeholder="misal: BBCA">
155
+ </div>
156
+ <div>
157
+ <label for="filterGlobal">Filter Global</label>
158
+ <input type="text" id="filterGlobal" placeholder="cari di semua kolom">
159
+ </div>
160
+ </div>
161
+
162
+ <div id="info"></div>
163
+ <div id="datasetInfo"></div>
164
+ </div>
165
+
166
+ <div class="wrap-table">
167
+ <table id="tbl"></table>
168
+ </div>
169
+ </div>
170
+
171
+ <!-- Analysis Tab -->
172
+ <div id="analysis" class="tab-content">
173
+ <div class="panel">
174
+ <h2>Analisis Data Saham</h2>
175
+ <p>Analisis historis berdasarkan semua data yang telah dikumpulkan</p>
176
+
177
+ <div class="row">
178
+ <div>
179
+ <label for="analysisStock">Analisis Saham Tertentu</label>
180
+ <input type="text" id="analysisStock" placeholder="Masukkan kode saham">
181
+ </div>
182
+ <div>
183
+ <button id="runAnalysis" class="bg-blue-100 text-blue-800 p-2 rounded mt-4">
184
+ <i class="fas fa-chart-line mr-1"></i>Jalankan Analisis
185
+ </button>
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ <div id="overallAnalysis" class="card">
191
+ <h3>Ringkasan Keseluruhan</h3>
192
+ <div class="chart-container">
193
+ <canvas id="overallChart"></canvas>
194
+ </div>
195
+
196
+ <div class="grid grid-cols-4 gap-4">
197
+ <div class="stats-item">
198
+ <div class="text-sm text-gray-600">Total Dataset</div>
199
+ <div id="totalDataset" class="stats-value">-</div>
200
+ </div>
201
+ <div class="stats-item">
202
+ <div class="text-sm text-gray-600">Total Saham Unik</div>
203
+ <div id="totalUniqueStocks" class="stats-value">-</div>
204
+ </div>
205
+ <div class="stats-item">
206
+ <div class="text-sm text-gray-600">Rata-rata Gain Harian</div>
207
+ <div id="avgDailyGain" class="stats-value">-</div>
208
+ </div>
209
+ <div class="stats-item">
210
+ <div class="text-sm text-gray-600">Volume Perdagangan</div>
211
+ <div id="totalVolume" class="stats-value">-</div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
216
+ <div id="stockAnalysis" class="card hidden">
217
+ <h3 id="stockAnalysisTitle">Analisis Saham</h3>
218
+ <div class="chart-container">
219
+ <canvas id="stockChart"></canvas>
220
+ </div>
221
+
222
+ <div class="grid grid-cols-2 gap-4">
223
+ <div>
224
+ <h4>Statistik Harga</h4>
225
+ <div class="stats-item">
226
+ <div class="text-sm text-gray-600">Perubahan Terbesar</div>
227
+ <div id="maxChange" class="stats-value">-</div>
228
+ </div>
229
+ <div class="stats-item">
230
+ <div class="text-sm text-gray-600">Perubahan Terkecil</div>
231
+ <div id="minChange" class="stats-value">-</div>
232
+ </div>
233
+ <div class="stats-item">
234
+ <div class="text-sm text-gray-600">Rata-rata Perubahan</div>
235
+ <div id="avgChange" class="stats-value">-</div>
236
+ </div>
237
+ <div class="stats-item">
238
+ <div class="text-sm text-gray-600">Tertinggi (Harga)</div>
239
+ <div id="highestPrice" class="stats-value">-</div>
240
+ </div>
241
+ </div>
242
+
243
+ <div>
244
+ <h4>Statistik Volume</h4>
245
+ <div class="stats-item">
246
+ <div class="text-sm text-gray-600">Volume Tertinggi</div>
247
+ <div id="highestVolume" class="stats-value">-</div>
248
+ </div>
249
+ <div class="stats-item">
250
+ <div class="text-sm text-gray-600">Rata-rata Volume</div>
251
+ <div id="avgVolume" class="stats-value">-</div>
252
+ </div>
253
+ <div class="stats-item">
254
+ <div class="text-sm text-gray-600">Total Volume</div>
255
+ <div id="totalStockVolume" class="stats-value">-</div>
256
+ </div>
257
+ <div class="stats-item">
258
+ <div class="text-sm text-gray-600">Trend (30 Hari)</div>
259
+ <div id="trend30d" class="stats-value">-</div>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+
266
+ <!-- Recommendations Tab -->
267
+ <div id="recommendations" class="tab-content">
268
+ <div class="panel">
269
+ <h2>Rekomendasi Saham</h2>
270
+ <p>Rekomendasi berdasarkan analisis data historis dan pola perdagangan</p>
271
+
272
+ <button id="generateRecommendations" class="bg-green-100 text-green-800 p-2 rounded mb-4">
273
+ <i class="fas fa-magic mr-1"></i>Hasilkan Rekomendasi
274
+ </button>
275
+ </div>
276
+
277
+ <div id="recommendationResults" class="hidden">
278
+ <div class="card">
279
+ <h3><i class="fas fa-fire text-red-500 mr-2"></i> Saham Potensial (High Momentum)</h3>
280
+ <table id="hotStocksTable" class="min-w-full"></table>
281
+ </div>
282
+
283
+ <div class="card">
284
+ <h3><i class="fas fa-chart-line text-blue-500 mr-2"></i> Saham Stabil</h3>
285
+ <table id="stableStocksTable" class="min-w-full"></table>
286
+ </div>
287
+
288
+ <div class="card">
289
+ <h3><i class="fas fa-level-up-alt text-green-500 mr-2"></i> Saham yang Mengalami Peningkatan Volume</h3>
290
+ <table id="volumeGainersTable" class="min-w-full"></table>
291
+ </div>
292
+
293
+ <div class="card">
294
+ <h3><i class="fas fa-clock text-yellow-500 mr-2"></i> Saham dengan Pola Musiman</h3>
295
+ <table id="seasonalStocksTable" class="min-w-full"></table>
296
+ </div>
297
+
298
+ <div class="card">
299
+ <h3><i class="fas fa-star text-purple-500 mr-2"></i> Rekomendasi Utama</h3>
300
+ <div id="topRecommendation" class="p-4 bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg"></div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+
305
+ <script>
306
+ // ====== KEY STORAGE ======
307
+ const LS_KEY = "idx_ringkasan_multi_xlsx_v3"; // versi baru
308
+
309
+ // ====== STATE ======
310
+ let currentDateKey = null;
311
+ let currentData = null; // {header:[], rows:[[]]}
312
+ let colMap = {};
313
+ let sortState = { colIndex: null, direction: 1 }; // 1 = asc, -1 = desc
314
+ let allDatasets = {};
315
+ let overallChart = null;
316
+ let stockChart = null;
317
+
318
+ // ====== STORAGE ======
319
+ function loadAllDatasets() {
320
+ try {
321
+ const raw = localStorage.getItem(LS_KEY);
322
+ if (!raw) return {};
323
+ const parsed = JSON.parse(raw);
324
+ // Verify data structure
325
+ for (const date in parsed) {
326
+ if (!parsed[date].header || !parsed[date].rows) {
327
+ delete parsed[date];
328
+ }
329
+ }
330
+ return parsed;
331
+ } catch (e) {
332
+ console.error("Error loading from localStorage:", e);
333
+ return {};
334
+ }
335
+ }
336
+
337
+ function saveAllDatasets(obj) {
338
+ try {
339
+ localStorage.setItem(LS_KEY, JSON.stringify(obj));
340
+ allDatasets = obj;
341
+ } catch (e) {
342
+ console.error("Error saving to localStorage:", e);
343
+ alert("Tidak dapat menyimpan data. Pastikan browser mengizinkan penyimpanan lokal.");
344
+ }
345
+ }
346
+
347
+ // ====== XLSX PARSER ======
348
+ function parseXlsxToRows(arrayBuffer) {
349
+ try {
350
+ const wb = XLSX.read(arrayBuffer, { type: "array" });
351
+ const sheetName = wb.SheetNames[0];
352
+ const sheet = wb.Sheets[sheetName];
353
+ const json = XLSX.utils.sheet_to_json(sheet, { header:1, raw:true });
354
+ if (!json.length) return { header: [], rows: [] };
355
+ const header = json[0].map(v => v != null ? String(v).trim() : "");
356
+ const rows = json.slice(1).map(r => r.map(v => v != null ? String(v).trim() : ""));
357
+ return { header, rows };
358
+ } catch (e) {
359
+ console.error("Error parsing XLSX:", e);
360
+ alert("Gagal membaca file Excel. Pastikan format file benar.");
361
+ return { header: [], rows: [] };
362
+ }
363
+ }
364
+
365
+ // ====== COLUMN MAP ======
366
+ function findColIdx(header, names) {
367
+ const lower = header.map(h => (h || "").toLowerCase());
368
+ for (const n of names) {
369
+ const idx = lower.indexOf(n.toLowerCase());
370
+ if (idx !== -1) return idx;
371
+ }
372
+ return -1;
373
+ }
374
+
375
+ function buildColMap(header) {
376
+ colMap = {
377
+ code: findColIdx(header, ["code","kode","stock","symbol","emiten"]),
378
+ pctChg: findColIdx(header, ["%change","% change","chg%","change%","%chg","chg %","perubahan (%)"]),
379
+ volume: findColIdx(header, ["volume","vol","lot"]),
380
+ value: findColIdx(header, ["value","nilai","val","turnover"]),
381
+ close: findColIdx(header, ["close","penutupan","harga penutupan","last","harga"]),
382
+ open: findColIdx(header, ["open","pembukaan","harga pembukaan"]),
383
+ high: findColIdx(header, ["high","tertinggi","harga tertinggi"]),
384
+ low: findColIdx(header, ["low","terendah","harga terendah"])
385
+ };
386
+ }
387
+
388
+ // ====== DATASET SELECT ======
389
+ function refreshDatasetSelect() {
390
+ const all = loadAllDatasets();
391
+ const select = document.getElementById("datasetSelect");
392
+ select.innerHTML = "";
393
+ const dates = Object.keys(all).sort();
394
+ dates.forEach(d => {
395
+ const opt = document.createElement("option");
396
+ opt.value = d;
397
+ opt.textContent = d;
398
+ select.appendChild(opt);
399
+ });
400
+ document.getElementById("filterBar").style.display = dates.length ? "flex" : "none";
401
+ }
402
+
403
+ document.getElementById("datasetSelect").addEventListener("change", () => {
404
+ const all = loadAllDatasets();
405
+ const key = document.getElementById("datasetSelect").value;
406
+ if (all[key]) {
407
+ currentDateKey = key;
408
+ currentData = all[key];
409
+ buildColMap(currentData.header);
410
+ sortState = { colIndex: null, direction: 1 };
411
+ renderTable();
412
+ }
413
+ });
414
+
415
+ // ====== SAVE DATASET (append jika tanggal baru) ======
416
+ function saveDataset(dateKey, parsed) {
417
+ if (!dateKey) {
418
+ document.getElementById("info").textContent = "Silakan isi tanggal perdagangan terlebih dahulu.";
419
+ return;
420
+ }
421
+ if (!parsed.header.length) {
422
+ document.getElementById("info").textContent = "File tidak berisi header/kolom.";
423
+ return;
424
+ }
425
+
426
+ const all = loadAllDatasets();
427
+ // overwrite jika tanggal sama, menambah kalau baru
428
+ all[dateKey] = parsed;
429
+ saveAllDatasets(all);
430
+
431
+ currentDateKey = dateKey;
432
+ currentData = parsed;
433
+ buildColMap(parsed.header);
434
+ refreshDatasetSelect();
435
+ const datasetSelect = document.getElementById("datasetSelect");
436
+ datasetSelect.value = dateKey;
437
+ document.getElementById("filterBar").style.display = "flex";
438
+ sortState = { colIndex: null, direction: 1 };
439
+ renderTable();
440
+ }
441
+
442
+ // ====== RENDER TABEL DENGAN SORT & FILTER ======
443
+ function renderTable() {
444
+ const tbl = document.getElementById("tbl");
445
+ const info = document.getElementById("info");
446
+ const datasetInfo = document.getElementById("datasetInfo");
447
+ tbl.innerHTML = "";
448
+
449
+ if (!currentData || !currentData.header.length) {
450
+ info.textContent = "Belum ada data. Upload file .xlsx terlebih dahulu.";
451
+ datasetInfo.textContent = "";
452
+ return;
453
+ }
454
+
455
+ const header = currentData.header;
456
+ const rows = currentData.rows.slice(); // copy
457
+
458
+ const codeFilter = document.getElementById("filterCode").value.trim().toLowerCase();
459
+ const globalFilter = document.getElementById("filterGlobal").value.trim().toLowerCase();
460
+
461
+ // filter
462
+ let filtered = rows.filter(r => {
463
+ if (!r || !r.length) return false;
464
+ let ok = true;
465
+ if (codeFilter && colMap.code >= 0) {
466
+ const code = (r[colMap.code] || "").toString().toLowerCase();
467
+ if (!code.includes(codeFilter)) ok = false;
468
+ }
469
+ if (ok && globalFilter) {
470
+ const joined = r.join(" ").toLowerCase();
471
+ if (!joined.includes(globalFilter)) ok = false;
472
+ }
473
+ return ok;
474
+ });
475
+
476
+ // helper angka
477
+ const num = v => {
478
+ if (v == null) return 0;
479
+ const s = v.toString().replace("%","").replace(/,/g,"").replace(/\s/g,"");
480
+ const n = parseFloat(s);
481
+ return isNaN(n) ? 0 : n;
482
+ };
483
+
484
+ // sort berdasarkan sortState
485
+ if (sortState.colIndex !== null) {
486
+ const idx = sortState.colIndex;
487
+ const dir = sortState.direction;
488
+ filtered.sort((a,b) => {
489
+ const av = a[idx] ?? "";
490
+ const bv = b[idx] ?? "";
491
+ const an = num(av);
492
+ const bn = num(bv);
493
+ // jika dua-duanya angka yang valid, pakai numeric saja
494
+ if (!isNaN(an) && !isNaN(bn)) {
495
+ return (an - bn) * dir;
496
+ }
497
+ // fallback string
498
+ return av.toString().localeCompare(bv.toString()) * dir;
499
+ });
500
+ }
501
+
502
+ // THEAD
503
+ const thead = document.createElement("thead");
504
+ const trh = document.createElement("tr");
505
+ header.forEach((h, idx) => {
506
+ const th = document.createElement("th");
507
+ th.textContent = h || ("Col " + (idx+1));
508
+
509
+ // label arah sort
510
+ if (sortState.colIndex === idx) {
511
+ const span = document.createElement("span");
512
+ span.className = "sort-ind";
513
+ span.textContent = sortState.direction === 1 ? "▲" : "▼";
514
+ th.appendChild(span);
515
+ }
516
+
517
+ // klik header untuk sort
518
+ th.addEventListener("click", () => {
519
+ if (sortState.colIndex === idx) {
520
+ sortState.direction *= -1; // toggle asc/desc
521
+ } else {
522
+ sortState.colIndex = idx;
523
+ sortState.direction = 1;
524
+ }
525
+ renderTable();
526
+ });
527
+
528
+ trh.appendChild(th);
529
+ });
530
+ thead.appendChild(trh);
531
+ tbl.appendChild(thead);
532
+
533
+ // TBODY
534
+ const tbody = document.createElement("tbody");
535
+ filtered.forEach(r => {
536
+ const tr = document.createElement("tr");
537
+ header.forEach((_, idx) => {
538
+ const td = document.createElement("td");
539
+ const val = r[idx] || "";
540
+ if (idx === colMap.pctChg) {
541
+ const n = num(val);
542
+ if (n > 0) {
543
+ td.classList.add("up");
544
+ td.innerHTML = `<i class="fas fa-caret-up mr-1"></i>${val}`;
545
+ }
546
+ if (n < 0) {
547
+ td.classList.add("down");
548
+ td.innerHTML = `<i class="fas fa-caret-down mr-1"></i>${val}`;
549
+ }
550
+ } else if (idx === colMap.code) {
551
+ td.innerHTML = `<span class="font-medium">${val}</span>`;
552
+ }
553
+ // Highlight high volume
554
+ else if (idx === colMap.volume && colMap.volume >= 0) {
555
+ const volume = num(val);
556
+ if (volume > 1000000) { // High volume threshold
557
+ td.innerHTML = `<span class="pill up-pill">${val}</span>`;
558
+ } else {
559
+ td.textContent = val;
560
+ }
561
+ }
562
+ else {
563
+ td.textContent = val;
564
+ }
565
+ tr.appendChild(td);
566
+ });
567
+ tbody.appendChild(tr);
568
+ });
569
+ tbl.appendChild(tbody);
570
+
571
+ info.innerHTML = `
572
+ <strong>Dataset aktif:</strong> ${currentDateKey || "-"}
573
+ | <strong>Baris data:</strong> ${filtered.length} (dari ${rows.length} total)
574
+ | <strong>Kolom:</strong> ${header.length}
575
+ `;
576
+
577
+ datasetInfo.innerHTML = `
578
+ <strong>Mapping kolom:</strong>
579
+ Code: <span class="pill">${colMap.code >= 0 ? colMap.code : '-'}</span>
580
+ %Change: <span class="pill">${colMap.pctChg >= 0 ? colMap.pctChg : '-'}</span>
581
+ Volume: <span class="pill">${colMap.volume >= 0 ? colMap.volume : '-'}</span>
582
+ Value: <span class="pill">${colMap.value >= 0 ? colMap.value : '-'}</span>
583
+ `;
584
+ }
585
+
586
+ // ====== Tab Navigation ======
587
+ document.querySelectorAll('.tab').forEach(tab => {
588
+ tab.addEventListener('click', () => {
589
+ // Remove active class from all tabs
590
+ document.querySelectorAll('.tab').forEach(t => {
591
+ t.classList.remove('active');
592
+ });
593
+ document.querySelectorAll('.tab-content').forEach(c => {
594
+ c.classList.remove('active');
595
+ });
596
+
597
+ // Add active class to clicked tab
598
+ tab.classList.add('active');
599
+ const tabId = tab.getAttribute('data-tab');
600
+ document.getElementById(tabId).classList.add('active');
601
+
602
+ // Refresh appropriate content
603
+ if (tabId === 'analysis') {
604
+ renderOverallAnalysis();
605
+ } else if (tabId === 'recommendations') {
606
+ // Nothing yet, will be populated when button is clicked
607
+ }
608
+ });
609
+ });
610
+
611
+ // ====== UPLOAD XLSX ======
612
+ document.getElementById("fileInput").addEventListener("change", e => {
613
+ const file = e.target.files[0];
614
+ if (!file) return;
615
+ const dateKey = document.getElementById("tradeDate").value;
616
+ const reader = new FileReader();
617
+ reader.onload = ev => {
618
+ const parsed = parseXlsxToRows(ev.target.result);
619
+ saveDataset(dateKey, parsed); // setelah ini otomatis render & bisa filter
620
+ };
621
+ reader.readAsArrayBuffer(file);
622
+ });
623
+
624
+ // ====== FILTER EVENT ======
625
+ document.getElementById("filterCode").addEventListener("input", renderTable);
626
+ document.getElementById("filterGlobal").addEventListener("input", renderTable);
627
+
628
+ // ====== CLEAR STORAGE ======
629
+ document.getElementById("clearStorage").addEventListener("click", () => {
630
+ if (confirm("Anda yakin ingin menghapus semua data? Tindakan ini tidak bisa dibatalkan.")) {
631
+ localStorage.removeItem(LS_KEY);
632
+ location.reload();
633
+ }
634
+ });
635
+
636
+ // ====== ANALYSIS FUNCTIONS ======
637
+ function renderOverallAnalysis() {
638
+ const allDatasets = loadAllDatasets();
639
+ const dates = Object.keys(allDatasets).sort();
640
+
641
+ if (dates.length === 0) {
642
+ document.getElementById("overallAnalysis").innerHTML = "<p class='text-red-500'>Tidak ada data untuk dianalisis. Silakan upload data terlebih dahulu.</p>";
643
+ return;
644
+ }
645
+
646
+ // Update summary statistics
647
+ document.getElementById("totalDataset").textContent = dates.length;
648
+
649
+ // Get unique stocks
650
+ const uniqueStocks = new Set();
651
+ let totalVolume = 0;
652
+ let allChanges = [];
653
+
654
+ for (const date of dates) {
655
+ const dataset = allDatasets[date];
656
+ const hdr = dataset.header;
657
+ const codeIdx = findColIdx(hdr, ["code","kode","stock","symbol"]);
658
+ const pctIdx = findColIdx(hdr, ["%change","% change","chg%","change%"]);
659
+ const volIdx = findColIdx(hdr, ["volume","vol"]);
660
+
661
+ for (const row of dataset.rows) {
662
+ if (codeIdx >= 0 && row[codeIdx]) {
663
+ uniqueStocks.add(row[codeIdx]);
664
+ }
665
+
666
+ if (pctIdx >= 0 && row[pctIdx]) {
667
+ const change = parseFloat(row[pctIdx].replace('%', '').replace(',', '.'));
668
+ if (!isNaN(change)) {
669
+ allChanges.push(change);
670
+ }
671
+ }
672
+
673
+ if (volIdx >= 0 && row[volIdx]) {
674
+ const vol = parseFloat(row[volIdx].replace(/,/g, ''));
675
+ if (!isNaN(vol)) {
676
+ totalVolume += vol;
677
+ }
678
+ }
679
+ }
680
+ }
681
+
682
+ document.getElementById("totalUniqueStocks").textContent = uniqueStocks.size;
683
+
684
+ // Calculate average daily gain
685
+ const avgGain = allChanges.length > 0 ? (allChanges.reduce((a, b) => a + b, 0) / allChanges.length).toFixed(2) : "0";
686
+ document.getElementById("avgDailyGain").textContent = `${avgGain}%`;
687
+
688
+ // Format total volume with appropriate units
689
+ let formattedVolume;
690
+ if (totalVolume >= 1e9) {
691
+ formattedVolume = (totalVolume / 1e9).toFixed(2) + " M";
692
+ } else if (totalVolume >= 1e6) {
693
+ formattedVolume = (totalVolume / 1e6).toFixed(2) + " Ribu";
694
+ } else {
695
+ formattedVolume = totalVolume.toLocaleString();
696
+ }
697
+ document.getElementById("totalVolume").textContent = formattedVolume;
698
+
699
+ // Create overall chart
700
+ const avgChangesPerDay = [];
701
+ const chartLabels = [];
702
+
703
+ for (const date of dates) {
704
+ const dataset = allDatasets[date];
705
+ const hdr = dataset.header;
706
+ const pctIdx = findColIdx(hdr, ["%change","% change","chg%","change%"]);
707
+
708
+ let dailyChanges = [];
709
+ for (const row of dataset.rows) {
710
+ if (pctIdx >= 0 && row[pctIdx]) {
711
+ const change = parseFloat(row[pctIdx].replace('%', '').replace(',', '.'));
712
+ if (!isNaN(change)) {
713
+ dailyChanges.push(change);
714
+ }
715
+ }
716
+ }
717
+
718
+ if (dailyChanges.length > 0) {
719
+ const dailyAvg = dailyChanges.reduce((a, b) => a + b, 0) / dailyChanges.length;
720
+ avgChangesPerDay.push(dailyAvg);
721
+ chartLabels.push(date.slice(5)); // Show only MM-DD
722
+ }
723
+ }
724
+
725
+ const ctx = document.getElementById('overallChart').getContext('2d');
726
+ if (overallChart) {
727
+ overallChart.destroy();
728
+ }
729
+
730
+ overallChart = new Chart(ctx, {
731
+ type: 'line',
732
+ data: {
733
+ labels: chartLabels,
734
+ datasets: [{
735
+ label: 'Rata-rata Perubahan Harian (%)',
736
+ data: avgChangesPerDay,
737
+ borderColor: avgChangesPerDay[avgChangesPerDay.length - 1] >= 0 ? 'rgb(0, 100, 0)' : 'rgb(178, 34, 34)',
738
+ backgroundColor: avgChangesPerDay[avgChangesPerDay.length - 1] >= 0 ? 'rgba(0, 100, 0, 0.1)' : 'rgba(178, 34, 34, 0.1)',
739
+ tension: 0.1,
740
+ fill: true
741
+ }]
742
+ },
743
+ options: {
744
+ responsive: true,
745
+ plugins: {
746
+ legend: {
747
+ position: 'top',
748
+ },
749
+ tooltip: {
750
+ callbacks: {
751
+ label: function(context) {
752
+ return `Rata-rata: ${context.parsed.y.toFixed(2)}%`;
753
+ }
754
+ }
755
+ }
756
+ },
757
+ scales: {
758
+ y: {
759
+ beginAtZero: true,
760
+ grid: {
761
+ color: 'rgba(0, 0, 0, 0.05)'
762
+ }
763
+ },
764
+ x: {
765
+ grid: {
766
+ color: 'rgba(0, 0, 0, 0.05)'
767
+ }
768
+ }
769
+ }
770
+ }
771
+ });
772
+
773
+ // Initially hide stock analysis
774
+ document.getElementById("stockAnalysis").classList.add("hidden");
775
+ }
776
+
777
+ function analyzeStock(code) {
778
+ const allDatasets = loadAllDatasets();
779
+ const dates = Object.keys(allDatasets).sort();
780
+ const stockData = [];
781
+
782
+ // Find the stock across all datasets
783
+ for (const date of dates) {
784
+ const dataset = allDatasets[date];
785
+ const codeIdx = findColIdx(dataset.header, ["code","kode","stock","symbol"]);
786
+ const pctIdx = findColIdx(dataset.header, ["%change","% change","chg%","change%"]);
787
+ const volIdx = findColIdx(dataset.header, ["volume","vol"]);
788
+ const closeIdx = findColIdx(dataset.header, ["close","penutupan","harga penutupan","last"]);
789
+
790
+ // Search for the stock in current dataset
791
+ for (const row of dataset.rows) {
792
+ if (codeIdx >= 0 && row[codeIdx] && row[codeIdx].toUpperCase() === code.toUpperCase()) {
793
+ const change = pctIdx >= 0 && row[pctIdx] ?
794
+ parseFloat(row[pctIdx].replace('%', '').replace(',', '.')) : 0;
795
+
796
+ const volume = volIdx >= 0 && row[volIdx] ?
797
+ parseFloat(row[volIdx].replace(/,/g, '')) : 0;
798
+
799
+ const close = closeIdx >= 0 && row[closeIdx] ?
800
+ parseFloat(row[closeIdx].replace(/,/g, '')) : 0;
801
+
802
+ stockData.push({
803
+ date: date,
804
+ change: change,
805
+ volume: volume,
806
+ close: close
807
+ });
808
+ break;
809
+ }
810
+ }
811
+ }
812
+
813
+ if (stockData.length === 0) {
814
+ alert(`Saham dengan kode ${code} tidak ditemukan dalam data yang tersedia.`);
815
+ return;
816
+ }
817
+
818
+ // Update UI with analysis
819
+ document.getElementById("stockAnalysisTitle").textContent = `Analisis Saham: ${code.toUpperCase()}`;
820
+ document.getElementById("stockAnalysis").classList.remove("hidden");
821
+
822
+ // Calculate statistics
823
+ const changes = stockData.map(d => d.change);
824
+ const maxChange = Math.max(...changes).toFixed(2);
825
+ const minChange = Math.min(...changes).toFixed(2);
826
+ const avgChange = (changes.reduce((a, b) => a + b, 0) / changes.length).toFixed(2);
827
+
828
+ const prices = stockData.map(d => d.close).filter(p => p > 0);
829
+ const highestPrice = prices.length > 0 ? Math.max(...prices).toLocaleString() : "-";
830
+
831
+ const volumes = stockData.map(d => d.volume);
832
+ const highestVolume = volumes.length > 0 ? Math.max(...volumes).toLocaleString() : "-";
833
+ const avgVolume = volumes.length > 0 ? (volumes.reduce((a, b) => a + b, 0) / volumes.length).toLocaleString() : "-";
834
+ const totalStockVolume = volumes.length > 0 ? volumes.reduce((a, b) => a + b, 0).toLocaleString() : "-";
835
+
836
+ // Calculate 30-day trend if possible
837
+ let trend30d = "-";
838
+ if (stockData.length >= 30) {
839
+ const recent30 = stockData.slice(-30);
840
+ const recentChanges = recent30.map(d => d.change);
841
+ const avg30 = recentChanges.reduce((a, b) => a + b, 0) / recentChanges.length;
842
+ trend30d = avg30 >= 0 ?
843
+ `<span class="up-pill pill"><i class="fas fa-caret-up mr-1"></i>${avg30.toFixed(2)}% (Bullish)</span>` :
844
+ `<span class="down-pill pill"><i class="fas fa-caret-down mr-1"></i>${avg30.toFixed(2)}% (Bearish)</span>`;
845
+ } else if (stockData.length > 1) {
846
+ const overallAvg = changes.reduce((a, b) => a + b, 0) / changes.length;
847
+ trend30d = overallAvg >= 0 ?
848
+ `<span class="up-pill pill"><i class="fas fa-caret-up mr-1"></i>${overallAvg.toFixed(2)}% (Bullish)</span>` :
849
+ `<span class="down-pill pill"><i class="fas fa-caret-down mr-1"></i>${overallAvg.toFixed(2)}% (Bearish)</span>`;
850
+ }
851
+
852
+ // Update stats
853
+ document.getElementById("maxChange").innerHTML = `<span class="up">${maxChange}%</span>`;
854
+ document.getElementById("minChange").innerHTML = `<span class="down">${minChange}%</span>`;
855
+ document.getElementById("avgChange").textContent = `${avgChange}%`;
856
+ document.getElementById("highestPrice").textContent = highestPrice;
857
+ document.getElementById("highestVolume").textContent = highestVolume;
858
+ document.getElementById("avgVolume").textContent = avgVolume;
859
+ document.getElementById("totalStockVolume").textContent = totalStockVolume;
860
+ document.getElementById("trend30d").innerHTML = trend30d;
861
+
862
+ // Create stock chart
863
+ const ctx = document.getElementById('stockChart').getContext('2d');
864
+ if (stockChart) {
865
+ stockChart.destroy();
866
+ }
867
+
868
+ // Prepare chart data
869
+ const chartLabels = stockData.map(d => d.date.slice(5)); // MM-DD format
870
+ const changeData = stockData.map(d => d.change);
871
+
872
+ // Determine colors based on last change
873
+ const lastChange = changeData[changeData.length - 1];
874
+ const borderColor = lastChange >= 0 ? 'rgb(0, 100, 0)' : 'rgb(178, 34, 34)';
875
+ const backgroundColor = lastChange >= 0 ? 'rgba(0, 100, 0, 0.1)' : 'rgba(178, 34, 34, 0.1)';
876
+
877
+ stockChart = new Chart(ctx, {
878
+ type: 'line',
879
+ data: {
880
+ labels: chartLabels,
881
+ datasets: [{
882
+ label: 'Perubahan Harian (%)',
883
+ data: changeData,
884
+ borderColor: borderColor,
885
+ backgroundColor: backgroundColor,
886
+ tension: 0.1,
887
+ fill: true
888
+ }]
889
+ },
890
+ options: {
891
+ responsive: true,
892
+ plugins: {
893
+ legend: {
894
+ position: 'top',
895
+ },
896
+ tooltip: {
897
+ callbacks: {
898
+ label: function(context) {
899
+ return `Perubahan: ${context.parsed.y.toFixed(2)}%`;
900
+ }
901
+ }
902
+ }
903
+ },
904
+ scales: {
905
+ y: {
906
+ beginAtZero: true,
907
+ grid: {
908
+ color: 'rgba(0, 0, 0, 0.05)'
909
+ }
910
+ },
911
+ x: {
912
+ grid: {
913
+ color: 'rgba(0, 0, 0, 0.05)'
914
+ }
915
+ }
916
+ }
917
+ }
918
+ });
919
+ }
920
+
921
+ // ====== STOCK ANALYSIS EVENT ======
922
+ document.getElementById("runAnalysis").addEventListener("click", () => {
923
+ const stockCode = document.getElementById("analysisStock").value.trim();
924
+ if (!stockCode) {
925
+ alert("Masukkan kode saham yang ingin dianalisis.");
926
+ return;
927
+ }
928
+ analyzeStock(stockCode);
929
+ });
930
+
931
+ // Enable pressing Enter to run analysis
932
+ document.getElementById("analysisStock").addEventListener("keypress", (e) => {
933
+ if (e.key === "Enter") {
934
+ document.getElementById("runAnalysis").click();
935
+ }
936
+ });
937
+
938
+ // ====== RECOMMENDATION SYSTEM ======
939
+ function generateRecommendations() {
940
+ const allDatasets = loadAllDatasets();
941
+ const dates = Object.keys(allDatasets).sort();
942
+
943
+ if (dates.length < 7) {
944
+ alert("Butuh minimal 7 hari data untuk menghasilkan rekomendasi.");
945
+ return;
946
+ }
947
+
948
+ // Show results container
949
+ document.getElementById("recommendationResults").classList.remove("hidden");
950
+
951
+ // Data structures for analysis
952
+ const stockPerformance = {}; // Aggregated performance data
953
+ const stockVolumeTrends = {}; // Volume trends
954
+ const seasonalPatterns = {}; // Monthly patterns
955
+ const recentActivity = {}; // Recent 7 days activity
956
+
957
+ // Go through all datasets to collect data
958
+ for (const date of dates) {
959
+ const dataset = allDatasets[date];
960
+ const codeIdx = findColIdx(dataset.header, ["code","kode","symbol"]);
961
+ const pctIdx = findColIdx(dataset.header, ["%change","% change","chg%"]);
962
+ const volIdx = findColIdx(dataset.header, ["volume","vol"]);
963
+
964
+ // Extract month for seasonal analysis
965
+ const month = date.slice(5, 7);
966
+ const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
967
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
968
+ const monthName = monthNames[parseInt(month) - 1];
969
+
970
+ // Process each row
971
+ for (const row of dataset.rows) {
972
+ if (codeIdx >= 0 && row[codeIdx]) {
973
+ const code = row[codeIdx].trim().toUpperCase();
974
+ if (!code) continue;
975
+
976
+ // Initialize data structures
977
+ if (!stockPerformance[code]) {
978
+ stockPerformance[code] = {
979
+ changes: [],
980
+ totalChange: 0,
981
+ positiveDays: 0,
982
+ negativeDays: 0,
983
+ totalDays: 0,
984
+ recentChanges: []
985
+ };
986
+ }
987
+
988
+ if (!stockVolumeTrends[code]) {
989
+ stockVolumeTrends[code] = {
990
+ volumes: [],
991
+ avgVolume: 0,
992
+ lastVolume: 0
993
+ };
994
+ }
995
+
996
+ if (!seasonalPatterns[monthName]) {
997
+ seasonalPatterns[monthName] = {
998
+ stocks: {},
999
+ avgChange: 0
1000
+ };
1001
+ }
1002
+
1003
+ if (!seasonalPatterns[monthName].stocks[code]) {
1004
+ seasonalPatterns[monthName].stocks[code] = {
1005
+ changes: []
1006
+ };
1007
+ }
1008
+
1009
+ if (dates.indexOf(date) >= dates.length - 7) {
1010
+ if (!recentActivity[code]) {
1011
+ recentActivity[code] = {
1012
+ changes: [],
1013
+ volumeChanges: []
1014
+ };
1015
+ }
1016
+ }
1017
+
1018
+ // Extract change value
1019
+ let change = 0;
1020
+ if (pctIdx >= 0 && row[pctIdx]) {
1021
+ const changeStr = row[pctIdx].replace('%', '').replace(',', '.');
1022
+ const parsedChange = parseFloat(changeStr);
1023
+ if (!isNaN(parsedChange)) {
1024
+ change = parsedChange;
1025
+ stockPerformance[code].changes.push(change);
1026
+ stockPerformance[code].totalChange += change;
1027
+ stockPerformance[code].totalDays++;
1028
+
1029
+ if (change > 0) stockPerformance[code].positiveDays++;
1030
+ if (change < 0) stockPerformance[code].negativeDays++;
1031
+
1032
+ // Add to recent activity if in last 7 days
1033
+ if (dates.indexOf(date) >= dates.length - 7) {
1034
+ recentActivity[code].changes.push(change);
1035
+ }
1036
+
1037
+ // Add to seasonal pattern
1038
+ seasonalPatterns[monthName].stocks[code].changes.push(change);
1039
+ }
1040
+ }
1041
+
1042
+ // Extract volume
1043
+ let volume = 0;
1044
+ if (volIdx >= 0 && row[volIdx]) {
1045
+ const volStr = row[volIdx].replace(/,/g, '');
1046
+ const parsedVol = parseFloat(volStr);
1047
+ if (!isNaN(parsedVol)) {
1048
+ volume = parsedVol;
1049
+ stockVolumeTrends[code].volumes.push(volume);
1050
+ stockVolumeTrends[code].lastVolume = volume;
1051
+
1052
+ // Add to recent activity
1053
+ if (dates.indexOf(date) >= dates.length - 7) {
1054
+ recentActivity[code].volumeChanges.push(volume);
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+
1062
+ // Calculate final metrics
1063
+ for (const code in stockPerformance) {
1064
+ const perf = stockPerformance[code];
1065
+ perf.avgChange = perf.totalDays > 0 ? perf.totalChange / perf.totalDays : 0;
1066
+ perf.winRate = perf.totalDays > 0 ? (perf.positiveDays / perf.totalDays) * 100 : 0;
1067
+
1068
+ // Calculate recent performance (last 7 days)
1069
+ const recentDays = Math.min(7, perf.changes.length);
1070
+ const recentChangePeriod = perf.changes.slice(-recentDays);
1071
+ perf.recentAvgChange = recentChangePeriod.length > 0 ?
1072
+ recentChangePeriod.reduce((a, b) => a + b, 0) / recentChangePeriod.length : 0;
1073
+ }
1074
+
1075
+ for (const code in stockVolumeTrends) {
1076
+ const vol = stockVolumeTrends[code];
1077
+ vol.avgVolume = vol.volumes.length > 0 ?
1078
+ vol.volumes.reduce((a, b) => a + b, 0) / vol.volumes.length : 0;
1079
+
1080
+ // Volume increase ratio
1081
+ vol.volumeRatio = vol.lastVolume > 0 && vol.avgVolume > 0 ?
1082
+ vol.lastVolume / vol.avgVolume : 0;
1083
+ }
1084
+
1085
+ // Calculate seasonal pattern averages
1086
+ for (const month in seasonalPatterns) {
1087
+ const monthData = seasonalPatterns[month];
1088
+ const allChanges = [];
1089
+
1090
+ for (const stock in monthData.stocks) {
1091
+ const stockChanges = monthData.stocks[stock].changes;
1092
+ if (stockChanges.length > 0) {
1093
+ const avgChange = stockChanges.reduce((a, b) => a + b, 0) / stockChanges.length;
1094
+ allChanges.push(avgChange);
1095
+ monthData.stocks[stock].avgChange = avgChange;
1096
+ }
1097
+ }
1098
+
1099
+ monthData.avgChange = allChanges.length > 0 ?
1100
+ allChanges.reduce((a, b) => a + b, 0) / allChanges.length : 0;
1101
+ }
1102
+
1103
+ // Generate HOT STOCKS - High momentum with recent positive movement
1104
+ const hotStocks = Object.entries(stockPerformance)
1105
+ .filter(([code, data]) => data.totalDays >= 5 && data.recentAvgChange > 1.5)
1106
+ .sort((a, b) => b[1].recentAvgChange - a[1].recentAvgChange)
1107
+ .slice(0, 10);
1108
+
1109
+ // Generate STABLE STOCKS - Consistent performers with low volatility
1110
+ const stableStocks = Object.entries(stockPerformance)
1111
+ .filter(([code, data]) => {
1112
+ // Need sufficient data and good win rate
1113
+ return data.totalDays >= 10 && data.winRate >= 55 &&
1114
+ // Low volatility (standard deviation of changes)
1115
+ data.changes.reduce((sum, change) => sum + Math.pow(change - data.avgChange, 2), 0) / data.changes.length < 4;
1116
+ })
1117
+ .sort((a, b) => b[1].avgChange - a[1].avgChange)
1118
+ .slice(0, 10);
1119
+
1120
+ // Generate VOLUME GAINERS - Unusually high volume with positive momentum
1121
+ const volumeGainers = Object.entries(stockPerformance)
1122
+ .filter(([code, data]) => {
1123
+ // Must have volume data and recent momentum
1124
+ return stockVolumeTrends[code] &&
1125
+ stockVolumeTrends[code].volumeRatio > 1.5 && // Volume at least 50% higher than average
1126
+ data.recentAvgChange > 0.5;
1127
+ })
1128
+ .sort((a, b) => {
1129
+ // Sort by volume ratio, then by recent performance
1130
+ const volA = stockVolumeTrends[a[0]].volumeRatio;
1131
+ const volB = stockVolumeTrends[b[0]].volumeRatio;
1132
+ const recentA = a[1].recentAvgChange;
1133
+ const recentB = b[1].recentAvgChange;
1134
+
1135
+ if (Math.abs(volB - volA) < 0.01) {
1136
+ return recentB - recentA;
1137
+ }
1138
+ return volB - volA;
1139
+ })
1140
+ .slice(0, 10);
1141
+
1142
+ // Generate SEASONAL STOCKS - Stocks with strong monthly patterns
1143
+ const seasonalStocks = [];
1144
+
1145
+ for (const month in seasonalPatterns) {
1146
+ const stocks = Object.entries(seasonalPatterns[month].stocks)
1147
+ .filter(([code, data]) => data.avgChange > 2 && data.avgChange > seasonalPatterns[month].avgChange)
1148
+ .sort((a, b) => b[1].avgChange - a[1].avgChange);
1149
+
1150
+ // Take top performers for each month
1151
+ for (let i = 0; i < Math.min(3, stocks.length); i++) {
1152
+ const [code, data] = stocks[i];
1153
+ seasonalStocks.push({
1154
+ code: code,
1155
+ month: month,
1156
+ avgChange: data.avgChange
1157
+ });
1158
+ }
1159
+ }
1160
+
1161
+ // Deduplicate while preserving order
1162
+ const seen = new Set();
1163
+ const uniqueSeasonalStocks = seasonalStocks.filter(item => {
1164
+ if (seen.has(item.code)) {
1165
+ return false;
1166
+ }
1167
+ seen.add(item.code);
1168
+ return true;
1169
+ }).slice(0, 10);
1170
+
1171
+ // Create TOP RECOMMENDATION based on multiple factors
1172
+ let topRecommendation = "";
1173
+ let highestConfidenceScore = 0;
1174
+
1175
+ // Analyze each stock for top recommendation
1176
+ for (const [code, data] of Object.entries(stockPerformance)) {
1177
+ if (data.totalDays < 5) continue;
1178
+
1179
+ // Check if this stock is in any of our top categories
1180
+ const inHotStocks = hotStocks.some(s => s[0] === code);
1181
+ const inVolumeGainers = volumeGainers.some(s => s[0] === code);
1182
+ const inStableStocks = stableStocks.some(s => s[0] === code);
1183
+ const inSeasonalStocks = uniqueSeasonalStocks.some(s => s.code === code);
1184
+
1185
+ // Calculate confidence score
1186
+ let confidenceScore = 0;
1187
+ let rationale = [];
1188
+
1189
+ if (inHotStocks) {
1190
+ confidenceScore += 3;
1191
+ rationale.push(`performa tinggi (${data.recentAvgChange.toFixed(2)}% rata-rata 7 hari terakhir)`);
1192
+ }
1193
+
1194
+ if (inVolumeGainers) {
1195
+ confidenceScore += 3;
1196
+ const volRatio = stockVolumeTrends[code].volumeRatio;
1197
+ rationale.push(`volume meningkat (${volRatio.toFixed(1)}x dari rata-rata)`);
1198
+ }
1199
+
1200
+ if (inStableStocks) {
1201
+ confidenceScore += 2;
1202
+ rationale.push(`stabil (tingkat kemenangan ${data.winRate.toFixed(1)}%)`);
1203
+ }
1204
+
1205
+ if (inSeasonalStocks) {
1206
+ confidenceScore += 2;
1207
+ const seasonalStock = uniqueSeasonalStocks.find(s => s.code === code);
1208
+ rationale.push(`potensi musiman ${seasonalStock.month}`);
1209
+ }
1210
+
1211
+ // Add bonus for consistency
1212
+ if (data.winRate > 60) {
1213
+ confidenceScore += 1;
1214
+ rationale.push("konsistensi tinggi");
1215
+ }
1216
+
1217
+ // Favor stocks with strong recent momentum
1218
+ if (data.recentAvgChange > 2) {
1219
+ confidenceScore += 2;
1220
+ } else if (data.recentAvgChange > 1) {
1221
+ confidenceScore += 1;
1222
+ }
1223
+
1224
+ if (confidenceScore > highestConfidenceScore) {
1225
+ highestConfidenceScore = confidenceScore;
1226
+ topRecommendation = `Saham <strong>${code}</strong> dinilai sebagai pilihan terbaik dengan skor kepercayaan ${confidenceScore}/10. ` +
1227
+ `Saham ini menunjukkan ${rationale.slice(0, 3).join(", ")}, yang menunjukkan potensi pertumbuhan jangka pendek. ` +
1228
+ `Dalam ${data.totalDays} hari terakhir, saham ini naik ${data.positiveDays} hari dan turun ${data.negativeDays} hari, ` +
1229
+ `dengan perubahan rata-rata ${data.avgChange.toFixed(2)}%.`;
1230
+ }
1231
+ }
1232
+
1233
+ // If no stock has strong qualities, provide a general recommendation
1234
+ if (!topRecommendation) {
1235
+ // Find the stock with the best recent performance
1236
+ const bestRecent = Object.entries(stockPerformance)
1237
+ .sort((a, b) => b[1].recentAvgChange - a[1].recentAvgChange)[0];
1238
+
1239
+ if (bestRecent) {
1240
+ topRecommendation = `Berdasarkan data terbaru, saham <strong>${bestRecent[0]}</strong> menunjukkan performa terbaik dengan perubahan rata-rata ${bestRecent[1].recentAvgChange.toFixed(2)}% ` +
1241
+ `selama periode terakhir. Kami merekomendasikan untuk memantau saham ini untuk peluang investasi jangka pendek.`;
1242
+ } else {
1243
+ topRecommendation = "Berdasarkan data yang tersedia, belum ada saham yang menunjukkan sinyal kuat untuk rekomendasi. " +
1244
+ "Disarankan untuk mengumpulkan lebih banyak data historis atau memantau saham dengan volume perdagangan tinggi " +
1245
+ "yang menunjukkan aktivitas investor institusi.";
1246
+ }
1247
+ }
1248
+
1249
+ // Render recommendation tables
1250
+ renderRecommendationTable("hotStocksTable", hotStocks,
1251
+ ["Kode", "Rata-rata 7 Hari (%)", "Total Hari", "Tingkat Kemenangan (%)"],
1252
+ (row) => [
1253
+ row[0],
1254
+ `<span class="up">${row[1].recentAvgChange.toFixed(2)}%</span>`,
1255
+ row[1].totalDays,
1256
+ `<span class="${row[1].winRate >= 60 ? 'up' : row[1].winRate >= 50 ? '' : 'down'}">${row[1].winRate.toFixed(1)}%</span>`
1257
+ ]);
1258
+
1259
+ renderRecommendationTable("stableStocksTable", stableStocks,
1260
+ ["Kode", "Rata-rata (%)", "Tingkat Kemenangan (%)", "Total Hari"],
1261
+ (row) => [
1262
+ row[0],
1263
+ `<span class="${row[1].avgChange >= 0 ? 'up' : 'down'}">${row[1].avgChange.toFixed(2)}%</span>`,
1264
+ `<span class="${row[1].winRate >= 60 ? 'up' : row[1].winRate >= 50 ? '' : 'down'}">${row[1].winRate.toFixed(1)}%</span>`,
1265
+ row[1].totalDays
1266
+ ]);
1267
+
1268
+ renderRecommendationTable("volumeGainersTable", volumeGainers,
1269
+ ["Kode", "Perubahan (%)", "Rasio Volume", "Volume Terakhir"],
1270
+ (row) => {
1271
+ const volData = stockVolumeTrends[row[0]];
1272
+ const lastVolume = volData.volumes[volData.volumes.length - 1];
1273
+ return [
1274
+ row[0],
1275
+ `<span class="up">${row[1].recentAvgChange.toFixed(2)}%</span>`,
1276
+ `<span class="up">${volData.volumeRatio.toFixed(2)}x</span>`,
1277
+ lastVolume.toLocaleString()
1278
+ ];
1279
+ });
1280
+
1281
+ renderRecommendationTable("seasonalStocksTable", uniqueSeasonalStocks,
1282
+ ["Kode", "Pola Bulanan", "Perubahan Rata-rata (%)", "Kategori"],
1283
+ (row) => [
1284
+ row.code,
1285
+ row.month,
1286
+ `<span class="up">${row.avgChange.toFixed(2)}%</span>`,
1287
+ "Pola Musiman"
1288
+ ]);
1289
+
1290
+ // Render top recommendation
1291
+ document.getElementById("topRecommendation").innerHTML = topRecommendation;
1292
+ }
1293
+
1294
+ function renderRecommendationTable(tableId, data, headers, rowMapper) {
1295
+ const table = document.getElementById(tableId);
1296
+ table.innerHTML = "";
1297
+
1298
+ if (data.length === 0) {
1299
+ table.innerHTML = "<tr><td class='p-2 text-gray-500'>Tidak ada data yang memenuhi kriteria</td></tr>";
1300
+ return;
1301
+ }
1302
+
1303
+ // Create header
1304
+ const thead = document.createElement("thead");
1305
+ const headerRow = document.createElement("tr");
1306
+ headers.forEach(header => {
1307
+ const th = document.createElement("th");
1308
+ th.textContent = header;
1309
+ th.className = "p-2 text-left bg-gray-100 font-semibold";
1310
+ headerRow.appendChild(th);
1311
+ });
1312
+ thead.appendChild(headerRow);
1313
+ table.appendChild(thead);
1314
+
1315
+ // Create body
1316
+ const tbody = document.createElement("tbody");
1317
+ data.forEach(row => {
1318
+ const tr = document.createElement("tr");
1319
+ tr.className = "border-t";
1320
+
1321
+ const cells = rowMapper(row);
1322
+ cells.forEach((cell, idx) => {
1323
+ const td = document.createElement("td");
1324
+ td.className = "p-2";
1325
+ td.innerHTML = cell;
1326
+ tr.appendChild(td);
1327
+ });
1328
+
1329
+ tbody.appendChild(tr);
1330
+ });
1331
+ table.appendChild(tbody);
1332
+ }
1333
+
1334
+ // Event listener for generate recommendations
1335
+ document.getElementById("generateRecommendations").addEventListener("click", generateRecommendations);
1336
+
1337
+ // ====== INIT LOAD DATASET LAMA ======
1338
+ (function init() {
1339
+ allDatasets = loadAllDatasets();
1340
+ refreshDatasetSelect();
1341
+ const dates = Object.keys(allDatasets).sort();
1342
+ if (dates.length) {
1343
+ currentDateKey = dates[dates.length - 1];
1344
+ currentData = allDatasets[currentDateKey];
1345
+ buildColMap(currentData.header);
1346
+ const datasetSelect = document.getElementById("datasetSelect");
1347
+ datasetSelect.value = currentDateKey;
1348
+ document.getElementById("filterBar").style.display = "flex";
1349
+ renderTable();
1350
+ }
1351
+ })();
1352
+ </script>
1353
+
1354
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=alterzick/broksum-v2" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1355
+ </html>
prompts.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ kenapa tidak terjadi fetch data yang sudah di upload