@anonympins/fingerprint 0.6.7 → 0.7.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/CHANGELOG.md +33 -0
- package/README.md +3 -7
- package/composer.json +8 -1
- package/package.json +1 -1
- package/public/fingerprint-wordpress.zip +0 -0
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +9 -11
- package/src/js/dynamic-wasm.js +7 -7
- package/src/js/fingerprint.builder.js +37 -38
- package/src/js/fingerprint.client.js +236 -152
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +517 -101
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +355 -384
- package/src/js/mongodb-store.js +1 -1
- package/src/js/optimization.worker.js +27 -27
- package/src/js/pow.solver.inline.js +206 -62
- package/src/js/pow.solver.js +235 -101
- package/src/js/pow.worker.js +48 -26
- package/src/js/redis-store.js +1 -1
- package/src/js/tests/fingerprint.client.init.test.js +146 -143
- package/src/js/tests/fingerprint.test.js +101 -40
- package/src/js/tests/ip-reputation.test.js +1 -0
- package/src/js/upow-model-task.js +14 -15
- package/src/php/AutoTuner.php +34 -34
- package/src/php/Challenge/ChallengeUtils.php +204 -32
- package/src/php/Config/SecurityProfiles.php +21 -0
- package/src/php/DirectFingerprint.php +190 -145
- package/src/php/FingerprintBuilder.php +29 -30
- package/src/php/FingerprintClient.php +24 -25
- package/src/php/FingerprintEngine.php +1836 -1880
- package/src/php/Ja3AnomalyDetector.php +258 -227
- package/src/php/Optimization/FunctionRegistry.php +7 -7
- package/src/php/Optimization/Optimization.php +24 -24
- package/src/php/Optimization/OptimizationOperators.php +28 -27
- package/src/php/Optimization/ProblemInitializers.php +52 -52
- package/src/php/ProblemManager.php +31 -34
- package/src/php/RequestContext.php +8 -1
- package/src/php/Store/IStore.php +7 -8
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +5 -5
- package/src/php/Store/RedisStore.php +5 -5
- package/src/php/Store/StoreManager.php +35 -35
- package/src/php/Tests/ChallengeUtilsTest.php +453 -453
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintEngineTest.php +662 -693
- package/src/php/Tests/IpReputationTest.php +10 -0
- package/src/php/Tests/MaliciousPatternsTest.php +103 -103
- package/src/php/Tests/MetricsTest.php +97 -97
- package/src/php/Tests/ProblemManagerTest.php +376 -376
- package/src/php/Tests/QuicFingerprintTest.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +471 -471
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
- package/src/php/Tests/config/ed25519_key.json +3 -3
- package/src/php/Utils/Env.php +49 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +236 -19
- package/src/php/Utils/TLSClientHelloParser.php +369 -212
- package/src/php/WordPress/WpDbStore.php +155 -0
- package/src/php/WordPress/fingerprint-wordpress.php +217 -0
- package/src/php/WordPress/package.php +123 -0
- package/src/php/bin/auto-tune.php +118 -118
package/src/js/library.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file @/library.js
|
|
3
|
-
* @description
|
|
4
|
-
*
|
|
3
|
+
* @description A library of tools based on the fundamental principle of dichotomy (division in two).
|
|
4
|
+
* Includes algorithms for sorted arrays, data structures, and problem solvers.
|
|
5
5
|
*
|
|
6
6
|
* @template T
|
|
7
7
|
* @callback Comparator
|
|
8
|
-
* @param {T} element -
|
|
9
|
-
* @param {any} target -
|
|
10
|
-
* @returns {number} -1
|
|
8
|
+
* @param {T} element - Array element.
|
|
9
|
+
* @param {any} target - Target value.
|
|
10
|
+
* @returns {number} -1 if element < target, 0 if element == target, 1 if element > target.
|
|
11
11
|
*
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -16,25 +16,25 @@ import os from "node:os";
|
|
|
16
16
|
import crypto from "node:crypto";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* Generates a cryptographically secure random float between 0 (inclusive) and 1 (exclusive).
|
|
20
20
|
* @returns {number}
|
|
21
21
|
*/
|
|
22
|
-
const random = () =>
|
|
22
|
+
const random = () => {
|
|
23
|
+
return crypto.randomInt(0, 4294967296) / 4294967296;
|
|
24
|
+
};
|
|
23
25
|
|
|
24
26
|
const Optimization = {
|
|
25
27
|
// eslint-disable-line no-unused-vars
|
|
26
28
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @
|
|
31
|
-
* @param {TSolution}
|
|
32
|
-
* @param {
|
|
33
|
-
* @param {
|
|
34
|
-
* @param {number} [
|
|
35
|
-
* @
|
|
36
|
-
* @param {number} [maxIterations=10000] - Le nombre total d'itérations.
|
|
37
|
-
* @returns {{solution: TSolution, energy: number}} Le meilleur couple solution/score trouvé.
|
|
29
|
+
* Finds an optimized solution using Simulated Annealing.
|
|
30
|
+
* @template TSolution - Type of the solution (number, array, object...).
|
|
31
|
+
* @param {TSolution} initialSolution - Starting point for optimization.
|
|
32
|
+
* @param {function(TSolution): number} evaluator - Evaluates a solution (goal is to MINIMIZE score).
|
|
33
|
+
* @param {function(TSolution): TSolution} neighbor - Generates a random neighbor solution.
|
|
34
|
+
* @param {number} [initialTemperature=1000] - Initial temperature.
|
|
35
|
+
* @param {number} [coolingRate=0.995] - Cooling rate factor.
|
|
36
|
+
* @param {number} [maxIterations=10000] - Total iterations.
|
|
37
|
+
* @returns {{solution: TSolution, energy: number}} Best solution and corresponding energy score.
|
|
38
38
|
*/
|
|
39
39
|
simulatedAnnealing(
|
|
40
40
|
initialSolution,
|
|
@@ -56,24 +56,24 @@ const Optimization = {
|
|
|
56
56
|
const newSolution = neighbor(currentSolution);
|
|
57
57
|
const newEnergy = evaluator(newSolution);
|
|
58
58
|
|
|
59
|
-
//
|
|
59
|
+
// Calculate acceptance probability for a worse solution
|
|
60
60
|
const acceptanceProbability = Math.exp(
|
|
61
61
|
(currentEnergy - newEnergy) / temperature,
|
|
62
62
|
);
|
|
63
63
|
|
|
64
|
-
//
|
|
64
|
+
// Decide whether to transition to the candidate solution
|
|
65
65
|
if (newEnergy < currentEnergy || random() < acceptanceProbability) {
|
|
66
66
|
currentSolution = newSolution;
|
|
67
67
|
currentEnergy = newEnergy;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
//
|
|
70
|
+
// Update global best found so far
|
|
71
71
|
if (currentEnergy < bestEnergy) {
|
|
72
72
|
bestSolution = currentSolution;
|
|
73
73
|
bestEnergy = currentEnergy;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
//
|
|
76
|
+
// Cool down temperature
|
|
77
77
|
temperature *= coolingRate;
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -81,21 +81,20 @@ const Optimization = {
|
|
|
81
81
|
},
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* @
|
|
87
|
-
* @param {function():
|
|
88
|
-
* @param {function(TChromosome):
|
|
89
|
-
* @param {function(TChromosome
|
|
90
|
-
* @param {
|
|
91
|
-
* @param {
|
|
92
|
-
* @param {number} [options.
|
|
93
|
-
* @param {number} [options.
|
|
94
|
-
* @param {number} [options.
|
|
95
|
-
* @param {
|
|
96
|
-
* @param {
|
|
97
|
-
* @
|
|
98
|
-
* @returns {{solution: TChromosome, fitness: number}} Le meilleur individu trouvé.
|
|
84
|
+
* Solves an optimization problem using a Genetic Algorithm.
|
|
85
|
+
* @template TChromosome - Type of the chromosome.
|
|
86
|
+
* @param {function(): TChromosome} createIndividual - Creates a random individual.
|
|
87
|
+
* @param {function(TChromosome): number} fitnessFunction - Evaluates an individual (MINIMIZATION).
|
|
88
|
+
* @param {function(TChromosome, TChromosome): TChromosome} crossover - Crosses two parents.
|
|
89
|
+
* @param {function(TChromosome): TChromosome} mutate - Applies random mutation.
|
|
90
|
+
* @param {object} options - Algorithm options.
|
|
91
|
+
* @param {number} [options.populationSize=100] - Population size.
|
|
92
|
+
* @param {number} [options.generations=100] - Number of generations.
|
|
93
|
+
* @param {number} [options.crossoverRate=0.8] - Crossover rate.
|
|
94
|
+
* @param {number} [options.mutationRate=0.1] - Mutation rate.
|
|
95
|
+
* @param {function} [options.selectionFunction] - Parent selection function.
|
|
96
|
+
* @param {boolean} [options.returnPopulation=false] - Whether to return final population instead of single best.
|
|
97
|
+
* @returns {{solution: TChromosome, fitness: number}} Best individual found.
|
|
99
98
|
*/
|
|
100
99
|
geneticAlgorithm(
|
|
101
100
|
createIndividual,
|
|
@@ -115,33 +114,30 @@ const Optimization = {
|
|
|
115
114
|
this.Operators.createTournamentSelection({ size: 5 });
|
|
116
115
|
const returnPopulation = options.returnPopulation || false;
|
|
117
116
|
|
|
118
|
-
// 1.
|
|
119
|
-
// La population est un tableau d'objets { chromosome, fitness }
|
|
120
|
-
// La fitness est calculée une seule fois par individu.
|
|
117
|
+
// 1. Initialization
|
|
121
118
|
let population = Array.from({ length: populationSize }, () => {
|
|
122
119
|
const chromosome = createIndividual();
|
|
123
120
|
return { chromosome, fitness: fitnessFunction(chromosome) };
|
|
124
121
|
});
|
|
125
122
|
|
|
126
|
-
//
|
|
123
|
+
// Sort initial population to find best
|
|
127
124
|
population.sort((a, b) => a.fitness - b.fitness);
|
|
128
125
|
let bestOverall = population[0];
|
|
129
126
|
|
|
130
|
-
// 2.
|
|
127
|
+
// 2. Generation loop
|
|
131
128
|
for (let gen = 0; gen < generations; gen++) {
|
|
132
129
|
const newPopulation = [];
|
|
133
130
|
|
|
134
|
-
//
|
|
135
|
-
// Il est déjà à l'index 0 grâce au tri à la fin de la boucle précédente.
|
|
131
|
+
// Elitism: retain best individual from previous generation
|
|
136
132
|
newPopulation.push(population[0]);
|
|
137
133
|
|
|
138
134
|
while (newPopulation.length < populationSize) {
|
|
139
|
-
// 3.
|
|
135
|
+
// 3. Selection
|
|
140
136
|
const parent1 = selectionFunction(population);
|
|
141
137
|
const parent2 = selectionFunction(population);
|
|
142
138
|
|
|
143
139
|
let offspringChromosome;
|
|
144
|
-
// 4.
|
|
140
|
+
// 4. Crossover
|
|
145
141
|
if (random() < crossoverRate) {
|
|
146
142
|
offspringChromosome = crossover(
|
|
147
143
|
parent1.chromosome,
|
|
@@ -156,21 +152,20 @@ const Optimization = {
|
|
|
156
152
|
offspringChromosome = mutate(offspringChromosome);
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
//
|
|
155
|
+
// Ensure operators returned a valid individual
|
|
160
156
|
if (offspringChromosome) {
|
|
161
157
|
newPopulation.push({
|
|
162
158
|
chromosome: offspringChromosome,
|
|
163
159
|
fitness: fitnessFunction(offspringChromosome),
|
|
164
160
|
});
|
|
165
161
|
} else {
|
|
166
|
-
// Si le croisement/mutation échoue, on réinsère un parent pour garder la taille de la population
|
|
167
162
|
newPopulation.push(parent1);
|
|
168
163
|
}
|
|
169
164
|
}
|
|
170
165
|
|
|
171
166
|
population = newPopulation;
|
|
172
167
|
|
|
173
|
-
//
|
|
168
|
+
// Sort new population for elitism and update best overall
|
|
174
169
|
population.sort((a, b) => a.fitness - b.fitness);
|
|
175
170
|
|
|
176
171
|
if (population[0].fitness < bestOverall.fitness) {
|
|
@@ -186,13 +181,11 @@ const Optimization = {
|
|
|
186
181
|
},
|
|
187
182
|
|
|
188
183
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* @param {
|
|
193
|
-
* @
|
|
194
|
-
* @param {boolean} [logProgress=false] - Si true, affiche le score de chaque cycle dans la console.
|
|
195
|
-
* @returns {{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}} Le meilleur résultat et des statistiques sur les exécutions.
|
|
184
|
+
* Executes a stochastic solver multiple times and returns the best result.
|
|
185
|
+
* @param {function(): {solution: any, energy?: number, fitness?: number}} solverFunction - Solver function.
|
|
186
|
+
* @param {number} numCycles - Number of execution cycles.
|
|
187
|
+
* @param {boolean} [logProgress=false] - Log progress to console.
|
|
188
|
+
* @returns {{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}} Best result and run stats.
|
|
196
189
|
*/
|
|
197
190
|
runMultiple(solverFunction, numCycles, logProgress = false) {
|
|
198
191
|
let bestResult = null;
|
|
@@ -201,8 +194,7 @@ const Optimization = {
|
|
|
201
194
|
for (let i = 0; i < numCycles; i++) {
|
|
202
195
|
const currentResult = solverFunction();
|
|
203
196
|
|
|
204
|
-
//
|
|
205
|
-
// On suppose que pour les deux, un score plus bas est meilleur.
|
|
197
|
+
// Handle simulated annealing (energy) and genetic algorithm (fitness) results (lower is better)
|
|
206
198
|
const currentScore =
|
|
207
199
|
currentResult.energy !== undefined
|
|
208
200
|
? currentResult.energy
|
|
@@ -211,7 +203,7 @@ const Optimization = {
|
|
|
211
203
|
|
|
212
204
|
if (logProgress) {
|
|
213
205
|
console.log(
|
|
214
|
-
` -> Cycle ${i + 1}/${numCycles}: Score
|
|
206
|
+
` -> Cycle ${i + 1}/${numCycles}: Score found = ${currentScore.toFixed(2)}`,
|
|
215
207
|
);
|
|
216
208
|
}
|
|
217
209
|
|
|
@@ -226,7 +218,7 @@ const Optimization = {
|
|
|
226
218
|
}
|
|
227
219
|
}
|
|
228
220
|
|
|
229
|
-
//
|
|
221
|
+
// Statistical calculations
|
|
230
222
|
const sum = allScores.reduce((a, b) => a + b, 0);
|
|
231
223
|
const average = sum / numCycles;
|
|
232
224
|
const variance =
|
|
@@ -244,16 +236,15 @@ const Optimization = {
|
|
|
244
236
|
},
|
|
245
237
|
|
|
246
238
|
/**
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* @
|
|
250
|
-
* @param {TSolution}
|
|
251
|
-
* @param {
|
|
252
|
-
* @param {
|
|
253
|
-
* @param {number} [options.
|
|
254
|
-
* @param {number} [options.
|
|
255
|
-
* @
|
|
256
|
-
* @returns {TSolution} La solution (minimum local) trouvée.
|
|
239
|
+
* Finds a local minimum of a differentiable function using Gradient Descent.
|
|
240
|
+
* @template TSolution - Solution type (number or array of numbers).
|
|
241
|
+
* @param {TSolution} initialSolution - Starting point.
|
|
242
|
+
* @param {function(TSolution): TSolution} gradientFunction - Computes gradient at given point.
|
|
243
|
+
* @param {object} options - Algorithm options.
|
|
244
|
+
* @param {number} [options.learningRate=0.01] - Step size.
|
|
245
|
+
* @param {number} [options.maxIterations=1000] - Max iterations.
|
|
246
|
+
* @param {number} [options.tolerance=1e-6] - Stopping threshold.
|
|
247
|
+
* @returns {TSolution} Local minimum found.
|
|
257
248
|
*/
|
|
258
249
|
gradientDescent(initialSolution, gradientFunction, options = {}) {
|
|
259
250
|
const {
|
|
@@ -280,7 +271,7 @@ const Optimization = {
|
|
|
280
271
|
);
|
|
281
272
|
if (change < tolerance) break;
|
|
282
273
|
} else {
|
|
283
|
-
//
|
|
274
|
+
// Single variable scalar case
|
|
284
275
|
const prevSolution = currentSolution;
|
|
285
276
|
currentSolution -= learningRate * gradient;
|
|
286
277
|
if (Math.abs(prevSolution - currentSolution) < tolerance) break;
|
|
@@ -290,15 +281,15 @@ const Optimization = {
|
|
|
290
281
|
},
|
|
291
282
|
|
|
292
283
|
/**
|
|
293
|
-
*
|
|
294
|
-
* @param {string} solverName -
|
|
295
|
-
* @param {Array<any>} baseSolverArgs -
|
|
296
|
-
* @param {number} numCycles -
|
|
297
|
-
* @param {boolean} [logProgress=false] -
|
|
298
|
-
* @param {object} [options={}] -
|
|
299
|
-
* @param {number} [options.concurrency] -
|
|
300
|
-
* @param {function(number): Array<any>} [options.workerDataGenerator] -
|
|
301
|
-
* @returns {Promise<{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}>}
|
|
284
|
+
* Executes a stochastic solver multiple times in parallel using a worker pool.
|
|
285
|
+
* @param {string} solverName - Name of the solver function in `Optimization.Operators`.
|
|
286
|
+
* @param {Array<any>} baseSolverArgs - Base arguments for solver.
|
|
287
|
+
* @param {number} numCycles - Total cycles to run.
|
|
288
|
+
* @param {boolean} [logProgress=false] - Log progress.
|
|
289
|
+
* @param {object} [options={}] - Concurrency options.
|
|
290
|
+
* @param {number} [options.concurrency] - Worker concurrency count (defaults to CPU count).
|
|
291
|
+
* @param {function(number): Array<any>} [options.workerDataGenerator] - Dynamic argument generator per task index.
|
|
292
|
+
* @returns {Promise<{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}>} Best result and stats.
|
|
302
293
|
*/
|
|
303
294
|
async runMultipleParallel(
|
|
304
295
|
solverName,
|
|
@@ -312,7 +303,7 @@ const Optimization = {
|
|
|
312
303
|
|
|
313
304
|
if (logProgress) {
|
|
314
305
|
console.log(
|
|
315
|
-
` (
|
|
306
|
+
` (Using a pool of ${concurrency} workers for ${numCycles} cycles with solver ${solverName})`,
|
|
316
307
|
);
|
|
317
308
|
}
|
|
318
309
|
|
|
@@ -333,15 +324,14 @@ const Optimization = {
|
|
|
333
324
|
};
|
|
334
325
|
|
|
335
326
|
const result = await new Promise(async (resolve, reject) => {
|
|
336
|
-
//
|
|
337
|
-
// On utilise import.meta.url pour résoudre le chemin de manière fiable.
|
|
327
|
+
// Resolve worker script path relative to current module
|
|
338
328
|
const worker = new Worker(new URL('./optimization.worker.js', import.meta.url), { workerData });
|
|
339
329
|
worker.on("message", resolve);
|
|
340
330
|
worker.on("error", reject);
|
|
341
331
|
worker.on("exit", (code) => {
|
|
342
332
|
if (code !== 0)
|
|
343
333
|
reject(
|
|
344
|
-
new Error(`Worker ${workerId}
|
|
334
|
+
new Error(`Worker ${workerId} exited with code ${code}`),
|
|
345
335
|
);
|
|
346
336
|
});
|
|
347
337
|
});
|
|
@@ -363,7 +353,7 @@ const Optimization = {
|
|
|
363
353
|
);
|
|
364
354
|
await Promise.all(workerPromises);
|
|
365
355
|
|
|
366
|
-
//
|
|
356
|
+
// Remainder follows runMultiple logic
|
|
367
357
|
let bestResult = null;
|
|
368
358
|
const allScores = [];
|
|
369
359
|
allResults.forEach((result) => {
|
|
@@ -395,30 +385,30 @@ const Optimization = {
|
|
|
395
385
|
};
|
|
396
386
|
|
|
397
387
|
/**
|
|
398
|
-
*
|
|
388
|
+
* Determines whether solution A Pareto-dominates solution B (minimization problem).
|
|
399
389
|
* @private
|
|
400
|
-
* @param {number[]} objectivesA -
|
|
401
|
-
* @param {number[]} objectivesB -
|
|
402
|
-
* @returns {boolean} - True
|
|
390
|
+
* @param {number[]} objectivesA - Objective scores for solution A.
|
|
391
|
+
* @param {number[]} objectivesB - Objective scores for solution B.
|
|
392
|
+
* @returns {boolean} - True if A dominates B.
|
|
403
393
|
*/
|
|
404
394
|
function paretoDominates(objectivesA, objectivesB) {
|
|
405
395
|
let aIsBetterInOne = false;
|
|
406
396
|
for (let i = 0; i < objectivesA.length; i++) {
|
|
407
397
|
if (objectivesA[i] > objectivesB[i]) {
|
|
408
|
-
return false; // A
|
|
398
|
+
return false; // A is worse on at least one objective
|
|
409
399
|
}
|
|
410
400
|
if (objectivesA[i] < objectivesB[i]) {
|
|
411
|
-
aIsBetterInOne = true; // A
|
|
401
|
+
aIsBetterInOne = true; // A is strictly better on at least one objective
|
|
412
402
|
}
|
|
413
403
|
}
|
|
414
|
-
return aIsBetterInOne;
|
|
404
|
+
return aIsBetterInOne;
|
|
415
405
|
}
|
|
416
406
|
|
|
417
407
|
/**
|
|
418
|
-
*
|
|
408
|
+
* Sorts a population into non-dominated Pareto fronts (inspired by NSGA-II).
|
|
419
409
|
* @private
|
|
420
|
-
* @param {Array<{individual: any, objectives: number[]}>} populationWithObjectives -
|
|
421
|
-
* @returns {Array<Array<{individual: any, objectives: number[]}>>} -
|
|
410
|
+
* @param {Array<{individual: any, objectives: number[]}>} populationWithObjectives - Population to sort.
|
|
411
|
+
* @returns {Array<Array<{individual: any, objectives: number[]}>>} - Array of fronts, first being the best.
|
|
422
412
|
*/
|
|
423
413
|
function nonDominatedSort(populationWithObjectives) {
|
|
424
414
|
const fronts = [[]];
|
|
@@ -460,9 +450,9 @@ function nonDominatedSort(populationWithObjectives) {
|
|
|
460
450
|
}
|
|
461
451
|
|
|
462
452
|
/**
|
|
463
|
-
*
|
|
453
|
+
* Calculates crowding distance for a front to preserve diversity.
|
|
464
454
|
* @private
|
|
465
|
-
* @param {Array<{individual: any, objectives: number[]}>} front -
|
|
455
|
+
* @param {Array<{individual: any, objectives: number[]}>} front - Pareto front.
|
|
466
456
|
*/
|
|
467
457
|
function calculateCrowdingDistance(front) {
|
|
468
458
|
if (front.length === 0) return;
|
|
@@ -474,7 +464,7 @@ function calculateCrowdingDistance(front) {
|
|
|
474
464
|
const minObj = front[0].objectives[i];
|
|
475
465
|
const maxObj = front[front.length - 1].objectives[i];
|
|
476
466
|
|
|
477
|
-
//
|
|
467
|
+
// Boundary solutions receive infinite distance to encourage spread
|
|
478
468
|
front[0].crowdingDistance = Infinity;
|
|
479
469
|
front[front.length - 1].crowdingDistance = Infinity;
|
|
480
470
|
|
|
@@ -489,13 +479,13 @@ function calculateCrowdingDistance(front) {
|
|
|
489
479
|
}
|
|
490
480
|
|
|
491
481
|
/**
|
|
492
|
-
*
|
|
493
|
-
* @param {function(): any} createIndividual -
|
|
494
|
-
* @param {function(any): number[]} fitnessFunction -
|
|
495
|
-
* @param {function(any, any): any} crossover -
|
|
496
|
-
* @param {function(any): any} mutate -
|
|
497
|
-
* @param {object} options -
|
|
498
|
-
* @returns {Array<{solution: any, objectives: number[]}>}
|
|
482
|
+
* Multi-objective genetic algorithm (inspired by NSGA-II) to find a Pareto front.
|
|
483
|
+
* @param {function(): any} createIndividual - Creates a random individual.
|
|
484
|
+
* @param {function(any): number[]} fitnessFunction - Evaluates individual and returns array of objectives to MINIMIZE.
|
|
485
|
+
* @param {function(any, any): any} crossover - Crossover function.
|
|
486
|
+
* @param {function(any): any} mutate - Mutation function.
|
|
487
|
+
* @param {object} options - Algorithm options.
|
|
488
|
+
* @returns {Array<{solution: any, objectives: number[]}>} Non-dominated Pareto front.
|
|
499
489
|
*/
|
|
500
490
|
Optimization.geneticAlgorithmMultiObjective = function (
|
|
501
491
|
createIndividual,
|
|
@@ -505,48 +495,47 @@ Optimization.geneticAlgorithmMultiObjective = function (
|
|
|
505
495
|
options = {},
|
|
506
496
|
) {
|
|
507
497
|
const {
|
|
508
|
-
generations = 150,
|
|
509
|
-
populationSize = 60,
|
|
498
|
+
generations = 150,
|
|
499
|
+
populationSize = 60,
|
|
510
500
|
mutationRate = 0.1,
|
|
511
501
|
currentConfig = null,
|
|
512
502
|
} = options;
|
|
513
503
|
|
|
514
504
|
let population = Array.from({ length: populationSize }, () => ({
|
|
515
505
|
individual: createIndividual(),
|
|
516
|
-
}));
|
|
506
|
+
}));
|
|
517
507
|
population.forEach((p) => (p.objectives = fitnessFunction(p.individual)));
|
|
518
508
|
|
|
519
509
|
for (let gen = 0; gen < generations; gen++) {
|
|
520
|
-
// 1.
|
|
510
|
+
// 1. Generate offspring
|
|
521
511
|
const offspring = [];
|
|
522
512
|
for (let i = 0; i < populationSize; i++) {
|
|
523
|
-
// Sélection simple pour l'exemple
|
|
524
513
|
const parent1 = population[crypto.randomInt(0, population.length)];
|
|
525
514
|
const parent2 = population[crypto.randomInt(0, population.length)];
|
|
526
515
|
let childIndividual = crossover(parent1.individual, parent2.individual);
|
|
527
516
|
if (random() < mutationRate) {
|
|
528
|
-
childIndividual = mutate(childIndividual, currentConfig);
|
|
517
|
+
childIndividual = mutate(childIndividual, currentConfig);
|
|
529
518
|
}
|
|
530
519
|
const child = { individual: childIndividual };
|
|
531
520
|
child.objectives = fitnessFunction(child.individual);
|
|
532
521
|
offspring.push(child);
|
|
533
522
|
}
|
|
534
523
|
|
|
535
|
-
// 2.
|
|
524
|
+
// 2. Combine parent and offspring populations
|
|
536
525
|
const combinedPopulation = [...population, ...offspring];
|
|
537
526
|
|
|
538
|
-
// 3.
|
|
527
|
+
// 3. Sort into Pareto fronts
|
|
539
528
|
const fronts = nonDominatedSort(combinedPopulation);
|
|
540
529
|
|
|
541
|
-
// 4.
|
|
530
|
+
// 4. Build next generation population
|
|
542
531
|
const newPopulation = [];
|
|
543
532
|
for (const front of fronts) {
|
|
544
533
|
if (newPopulation.length + front.length <= populationSize) {
|
|
545
534
|
newPopulation.push(...front);
|
|
546
535
|
} else {
|
|
547
|
-
//
|
|
536
|
+
// Use crowding distance truncation if front overflows population size
|
|
548
537
|
calculateCrowdingDistance(front);
|
|
549
|
-
front.sort((a, b) => b.crowdingDistance - a.crowdingDistance);
|
|
538
|
+
front.sort((a, b) => b.crowdingDistance - a.crowdingDistance);
|
|
550
539
|
const remaining = populationSize - newPopulation.length;
|
|
551
540
|
newPopulation.push(...front.slice(0, remaining));
|
|
552
541
|
break;
|
|
@@ -555,11 +544,11 @@ Optimization.geneticAlgorithmMultiObjective = function (
|
|
|
555
544
|
population = newPopulation;
|
|
556
545
|
}
|
|
557
546
|
|
|
558
|
-
//
|
|
547
|
+
// Return the first front of the final population
|
|
559
548
|
const finalFronts = nonDominatedSort(population);
|
|
560
549
|
const bestFront = finalFronts.length > 0 ? finalFronts[0] : [];
|
|
561
550
|
|
|
562
|
-
//
|
|
551
|
+
// Deduplicate solutions with identical objectives
|
|
563
552
|
const uniqueSolutionsMap = new Map();
|
|
564
553
|
for (const p of bestFront) {
|
|
565
554
|
const key = JSON.stringify(p.objectives);
|
|
@@ -574,68 +563,68 @@ Optimization.geneticAlgorithmMultiObjective = function (
|
|
|
574
563
|
};
|
|
575
564
|
|
|
576
565
|
/**
|
|
577
|
-
*
|
|
566
|
+
* Optimization utilities.
|
|
578
567
|
*/
|
|
579
568
|
Optimization.Utils = {
|
|
580
|
-
/**
|
|
569
|
+
/** Calculates Euclidean distance between two points/cities. */
|
|
581
570
|
distance: (city1, city2) => Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2)),
|
|
582
571
|
|
|
583
|
-
/**
|
|
572
|
+
/** Evaluates the total round-trip distance of a TSP path. */
|
|
584
573
|
evaluatePathDistance: (cities, path) => {
|
|
585
574
|
let totalDistance = 0;
|
|
586
575
|
for (let i = 0; i < path.length - 1; i++) {
|
|
587
576
|
totalDistance += Optimization.Utils.distance(cities[path[i]], cities[path[i + 1]]);
|
|
588
577
|
}
|
|
589
|
-
totalDistance += Optimization.Utils.distance(cities[path[path.length - 1]], cities[path[0]]); //
|
|
578
|
+
totalDistance += Optimization.Utils.distance(cities[path[path.length - 1]], cities[path[0]]); // Return to start
|
|
590
579
|
return totalDistance;
|
|
591
580
|
}
|
|
592
581
|
};
|
|
593
582
|
|
|
594
583
|
/**
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
* @param {function(Array<number>): number} fitnessFunction -
|
|
599
|
-
* @param {Array<number>} initialSolution -
|
|
600
|
-
* @param {number} initialStepSize -
|
|
601
|
-
* @param {object} [options={}] -
|
|
602
|
-
* @param {number} [options.maxGenerations=100] -
|
|
603
|
-
* @param {number} [options.populationSize] -
|
|
604
|
-
* @param {number} [options.tolerance=1e-6] -
|
|
605
|
-
* @returns {{solution: Array<number>, fitness: number}}
|
|
584
|
+
* CMA-ES (Covariance Matrix Adaptation Evolution Strategy) optimization algorithm.
|
|
585
|
+
* State-of-the-art evolutionary algorithm for black-box optimization of non-linear, non-convex functions.
|
|
586
|
+
* Particularly effective for problems involving continuous real-valued variables.
|
|
587
|
+
* @param {function(Array<number>): number} fitnessFunction - The objective function to MINIMIZE.
|
|
588
|
+
* @param {Array<number>} initialSolution - Starting point for the search (numeric vector).
|
|
589
|
+
* @param {number} initialStepSize - Initial search step size (sigma).
|
|
590
|
+
* @param {object} [options={}] - Algorithm options.
|
|
591
|
+
* @param {number} [options.maxGenerations=100] - Maximum number of generations.
|
|
592
|
+
* @param {number} [options.populationSize] - Population size (lambda). Automatically computed if omitted.
|
|
593
|
+
* @param {number} [options.tolerance=1e-6] - Tolerance threshold for early stopping.
|
|
594
|
+
* @returns {{solution: Array<number>, fitness: number}} Best solution found and its fitness score.
|
|
606
595
|
*/
|
|
607
596
|
Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize, options = {}) {
|
|
608
|
-
const n = initialSolution.length; //
|
|
597
|
+
const n = initialSolution.length; // Problem dimension
|
|
609
598
|
|
|
610
|
-
// ---
|
|
599
|
+
// --- Algorithm strategy parameters ---
|
|
611
600
|
const { maxGenerations = 100, tolerance = 1e-6 } = options;
|
|
612
601
|
const populationSize = options.populationSize || (4 + Math.floor(3 * Math.log(n))); // Lambda
|
|
613
|
-
const mu = Math.floor(populationSize / 2); //
|
|
602
|
+
const mu = Math.floor(populationSize / 2); // Parent count for recombination
|
|
614
603
|
|
|
615
|
-
//
|
|
604
|
+
// Recombination weights
|
|
616
605
|
let weights = Array.from({ length: mu }, (_, i) => Math.log(mu + 0.5) - Math.log(i + 1));
|
|
617
606
|
const sumWeights = weights.reduce((s, w) => s + w, 0);
|
|
618
607
|
weights = weights.map(w => w / sumWeights);
|
|
619
608
|
const muEff = 1 / weights.reduce((s, w) => s + w * w, 0);
|
|
620
609
|
|
|
621
|
-
//
|
|
610
|
+
// Adaptation parameters
|
|
622
611
|
const cc = (4 + muEff / n) / (n + 4 + 2 * muEff / n);
|
|
623
612
|
const cs = (muEff + 2) / (n + muEff + 5);
|
|
624
613
|
const c1 = 2 / (Math.pow(n + 1.3, 2) + muEff);
|
|
625
614
|
const cmu = Math.min(1 - c1, 2 * (muEff - 2 + 1 / muEff) / (Math.pow(n + 2, 2) + muEff));
|
|
626
615
|
const damps = 1 + 2 * Math.max(0, Math.sqrt((muEff - 1) / (n + 1)) - 1) + cs;
|
|
627
616
|
|
|
628
|
-
// ---
|
|
629
|
-
let mean = [...initialSolution]; //
|
|
617
|
+
// --- Dynamic state variables ---
|
|
618
|
+
let mean = [...initialSolution]; // Distribution mean / search center
|
|
630
619
|
let stepSize = initialStepSize; // Sigma
|
|
631
|
-
let C = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))); //
|
|
632
|
-
let pc = Array(n).fill(0); //
|
|
633
|
-
let ps = Array(n).fill(0); //
|
|
620
|
+
let C = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))); // Covariance matrix
|
|
621
|
+
let pc = Array(n).fill(0); // Evolution path for C
|
|
622
|
+
let ps = Array(n).fill(0); // Evolution path for sigma
|
|
634
623
|
|
|
635
624
|
let bestFitness = Infinity;
|
|
636
625
|
let bestSolution = null;
|
|
637
626
|
|
|
638
|
-
//
|
|
627
|
+
// Cholesky decomposition helper (simplified for symmetric positive-definite matrices)
|
|
639
628
|
function cholesky(A) {
|
|
640
629
|
const L = Array.from({ length: n }, () => Array(n).fill(0));
|
|
641
630
|
for (let i = 0; i < n; i++) {
|
|
@@ -646,7 +635,7 @@ Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize,
|
|
|
646
635
|
}
|
|
647
636
|
if (i === j) {
|
|
648
637
|
const val = A[i][i] - sum;
|
|
649
|
-
if (val < 0) return null; //
|
|
638
|
+
if (val < 0) return null; // Not positive definite
|
|
650
639
|
L[i][j] = Math.sqrt(val);
|
|
651
640
|
} else {
|
|
652
641
|
if (L[j][j] === 0) return null;
|
|
@@ -658,18 +647,18 @@ Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize,
|
|
|
658
647
|
}
|
|
659
648
|
|
|
660
649
|
for (let gen = 0; gen < maxGenerations; gen++) {
|
|
661
|
-
// 1.
|
|
650
|
+
// 1. Sample new population
|
|
662
651
|
const population = [];
|
|
663
|
-
const arx = []; //
|
|
652
|
+
const arx = []; // Search vectors
|
|
664
653
|
const L = cholesky(C);
|
|
665
654
|
if (!L) {
|
|
666
|
-
console.warn("[CMA-ES]
|
|
655
|
+
console.warn("[CMA-ES] Covariance matrix is no longer positive definite. Stopping.");
|
|
667
656
|
break;
|
|
668
657
|
}
|
|
669
658
|
|
|
670
659
|
for (let i = 0; i < populationSize; i++) {
|
|
671
|
-
const z = Array.from({ length: n }, () => random() * 2 - 1); //
|
|
672
|
-
const y = Array(n).fill(0); // z
|
|
660
|
+
const z = Array.from({ length: n }, () => random() * 2 - 1); // Standard normal approximation
|
|
661
|
+
const y = Array(n).fill(0); // z transformed by L
|
|
673
662
|
for (let r = 0; r < n; r++) {
|
|
674
663
|
for (let c = 0; c < n; c++) {
|
|
675
664
|
y[r] += L[r][c] * z[c];
|
|
@@ -680,7 +669,7 @@ Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize,
|
|
|
680
669
|
population.push({ individual, fitness: fitnessFunction(individual) });
|
|
681
670
|
}
|
|
682
671
|
|
|
683
|
-
// 2.
|
|
672
|
+
// 2. Sort and select elite parents
|
|
684
673
|
population.sort((a, b) => a.fitness - b.fitness);
|
|
685
674
|
const parents = population.slice(0, mu);
|
|
686
675
|
|
|
@@ -689,7 +678,7 @@ Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize,
|
|
|
689
678
|
bestSolution = parents[0].individual;
|
|
690
679
|
}
|
|
691
680
|
|
|
692
|
-
// 3.
|
|
681
|
+
// 3. Update distribution mean
|
|
693
682
|
const oldMean = [...mean];
|
|
694
683
|
const y_w = Array(n).fill(0);
|
|
695
684
|
for (let j = 0; j < n; j++) {
|
|
@@ -700,15 +689,15 @@ Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize,
|
|
|
700
689
|
}
|
|
701
690
|
mean = oldMean.map((m, i) => m + stepSize * y_w[i]);
|
|
702
691
|
|
|
703
|
-
// 4.
|
|
704
|
-
const C_inv_sqrt = cholesky(C); // Simplification,
|
|
692
|
+
// 4. Adapt evolution paths
|
|
693
|
+
const C_inv_sqrt = cholesky(C); // Simplification, ideally inverse square root
|
|
705
694
|
const C_inv_sqrt_y_w = y_w; // Approximation
|
|
706
695
|
ps = ps.map((p, i) => (1 - cs) * p + Math.sqrt(cs * (2 - cs) * muEff) * C_inv_sqrt_y_w[i]);
|
|
707
696
|
|
|
708
697
|
const hsig = Math.sqrt(ps.reduce((s, v) => s + v*v, 0)) / (1 - Math.pow(1 - cs, 2 * (gen + 1))) / n < 1.4 + 2 / (n + 1);
|
|
709
698
|
pc = pc.map((p, i) => (1 - cc) * p + (hsig ? Math.sqrt(cc * (2 - cc) * muEff) * y_w[i] : 0));
|
|
710
699
|
|
|
711
|
-
// 5.
|
|
700
|
+
// 5. Adapt covariance matrix C
|
|
712
701
|
let rankOneUpdate = Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => c1 * pc[i] * pc[j]));
|
|
713
702
|
let rankMuUpdate = Array.from({ length: n }, () => Array(n).fill(0));
|
|
714
703
|
for (let k = 0; k < mu; k++) {
|
|
@@ -721,7 +710,7 @@ Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize,
|
|
|
721
710
|
}
|
|
722
711
|
C = C.map((row, i) => row.map((val, j) => (1 - c1 - cmu) * val + rankOneUpdate[i][j] + rankMuUpdate[i][j]));
|
|
723
712
|
|
|
724
|
-
// 6.
|
|
713
|
+
// 6. Adapt step size (sigma)
|
|
725
714
|
stepSize *= Math.exp((cs / damps) * (Math.sqrt(ps.reduce((s, v) => s + v*v, 0)) / Math.sqrt(n) - 1));
|
|
726
715
|
}
|
|
727
716
|
|
|
@@ -730,16 +719,16 @@ Optimization.cmaes = function(fitnessFunction, initialSolution, initialStepSize,
|
|
|
730
719
|
|
|
731
720
|
/**
|
|
732
721
|
* @namespace Optimization.Operators
|
|
733
|
-
* @description
|
|
734
|
-
*
|
|
722
|
+
* @description Evaluator factories for complex, multi-dimensional optimization problems
|
|
723
|
+
* designed for use with Optimization solvers (Simulated Annealing, Genetic Algorithms, etc.).
|
|
735
724
|
*/
|
|
736
|
-
Optimization.Operators = {}; //
|
|
725
|
+
Optimization.Operators = {}; // Namespace creation
|
|
737
726
|
|
|
738
727
|
/**
|
|
739
|
-
*
|
|
740
|
-
* @param {object} [options] -
|
|
741
|
-
* @param {number} [options.size=5] -
|
|
742
|
-
* @returns {function(Array<{chromosome: any, fitness: number}>): {chromosome: any, fitness: number}}
|
|
728
|
+
* Creates a tournament selection operator for genetic algorithms.
|
|
729
|
+
* @param {object} [options] - Tournament options.
|
|
730
|
+
* @param {number} [options.size=5] - Number of candidates per tournament.
|
|
731
|
+
* @returns {function(Array<{chromosome: any, fitness: number}>): {chromosome: any, fitness: number}} Tournament selection operator.
|
|
743
732
|
*/
|
|
744
733
|
Optimization.Operators.createTournamentSelection = (options = {}) => {
|
|
745
734
|
const tournamentSize = options.size || 5;
|
|
@@ -754,8 +743,8 @@ Optimization.Operators.createTournamentSelection = (options = {}) => {
|
|
|
754
743
|
best = individual;
|
|
755
744
|
}
|
|
756
745
|
}
|
|
757
|
-
//
|
|
758
|
-
//
|
|
746
|
+
// Returns the best candidate. In degenerate edge cases (all fitness scores are Infinity),
|
|
747
|
+
// fallback to a random candidate instead of returning null.
|
|
759
748
|
if (!best) {
|
|
760
749
|
return population[crypto.randomInt(0, population.length)];
|
|
761
750
|
}
|
|
@@ -764,12 +753,12 @@ Optimization.Operators.createTournamentSelection = (options = {}) => {
|
|
|
764
753
|
};
|
|
765
754
|
|
|
766
755
|
/**
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
* @param {object} config -
|
|
770
|
-
* @param {Array<{name: string, volatility: number}>} config.assets -
|
|
771
|
-
* @param {Array<{assets: [string, string], correlation: number}>} config.correlations -
|
|
772
|
-
* @returns {Array<Array<number>>}
|
|
756
|
+
* Creates a covariance matrix from pairwise asset correlations.
|
|
757
|
+
* Provides an intuitive representation for cross-asset risk modeling.
|
|
758
|
+
* @param {object} config - Configuration object.
|
|
759
|
+
* @param {Array<{name: string, volatility: number}>} config.assets - Asset definitions with volatilities.
|
|
760
|
+
* @param {Array<{assets: [string, string], correlation: number}>} config.correlations - Pairwise correlation entries.
|
|
761
|
+
* @returns {Array<Array<number>>} Resulting covariance matrix.
|
|
773
762
|
*/
|
|
774
763
|
Optimization.Operators.createCovarianceMatrixFromCorrelations = ({
|
|
775
764
|
assets,
|
|
@@ -778,24 +767,24 @@ Optimization.Operators.createCovarianceMatrixFromCorrelations = ({
|
|
|
778
767
|
const n = assets.length;
|
|
779
768
|
const matrix = Array.from({ length: n }, () => Array(n).fill(0));
|
|
780
769
|
|
|
781
|
-
//
|
|
770
|
+
// Fast lookup map for asset details by name
|
|
782
771
|
const assetInfo = new Map();
|
|
783
772
|
assets.forEach((asset, index) => {
|
|
784
773
|
assetInfo.set(asset.name, { index, volatility: asset.volatility });
|
|
785
774
|
});
|
|
786
775
|
|
|
787
|
-
// 1.
|
|
776
|
+
// 1. Fill diagonal with individual variances (volatility^2)
|
|
788
777
|
for (let i = 0; i < n; i++) {
|
|
789
778
|
const variance = Math.pow(assets[i].volatility, 2);
|
|
790
779
|
matrix[i][i] = variance;
|
|
791
780
|
}
|
|
792
781
|
|
|
793
|
-
// 2.
|
|
782
|
+
// 2. Fill off-diagonal cells with computed covariances
|
|
794
783
|
for (const corr of correlations) {
|
|
795
784
|
const [nameA, nameB] = corr.assets;
|
|
796
785
|
if (!assetInfo.has(nameA) || !assetInfo.has(nameB)) {
|
|
797
786
|
console.warn(
|
|
798
|
-
`
|
|
787
|
+
`Warning: One of assets [${nameA}, ${nameB}] was not found. Correlation ignored.`,
|
|
799
788
|
);
|
|
800
789
|
continue;
|
|
801
790
|
}
|
|
@@ -807,13 +796,13 @@ Optimization.Operators.createCovarianceMatrixFromCorrelations = ({
|
|
|
807
796
|
const covariance = corr.correlation * infoA.volatility * infoB.volatility;
|
|
808
797
|
|
|
809
798
|
matrix[infoA.index][infoB.index] = covariance;
|
|
810
|
-
matrix[infoB.index][infoA.index] = covariance; //
|
|
799
|
+
matrix[infoB.index][infoA.index] = covariance; // Symmetric matrix
|
|
811
800
|
}
|
|
812
801
|
|
|
813
802
|
return matrix;
|
|
814
803
|
};
|
|
815
804
|
|
|
816
|
-
//
|
|
805
|
+
// Market imbalance is the absolute delta between supply and demand (goal is minimization)
|
|
817
806
|
Optimization.Operators.createMarketEquilibriumEvaluator = (
|
|
818
807
|
demandModel,
|
|
819
808
|
supplyModel,
|
|
@@ -825,26 +814,26 @@ Optimization.Operators.createMarketEquilibriumEvaluator = (
|
|
|
825
814
|
};
|
|
826
815
|
};
|
|
827
816
|
|
|
828
|
-
// Un "individu" est un tableau de 3 poids (ex: [0.5, 0.2, 0.3]) qui doivent sommer à 1.
|
|
829
817
|
/**
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
* @param {
|
|
833
|
-
* @param {number} config.
|
|
834
|
-
* @param {
|
|
835
|
-
* @
|
|
818
|
+
* Creates a fitness function for portfolio allocation.
|
|
819
|
+
* An individual is a weight array (e.g. [0.5, 0.2, 0.3]) normalized to sum to 1.
|
|
820
|
+
* @param {object} config - Configuration object.
|
|
821
|
+
* @param {Array<{name: string, expectedReturn: number, volatility: number}>} config.assets - Target assets.
|
|
822
|
+
* @param {number} config.maxVolatility - Maximum portfolio volatility threshold constraint.
|
|
823
|
+
* @param {Array<Array<number>>} [config.covarianceMatrix] - Covariance matrix for exact variance evaluation.
|
|
824
|
+
* @returns {function(Array<number>): number} Fitness evaluator returning negative return with risk penalty.
|
|
836
825
|
*/
|
|
837
826
|
Optimization.Operators.createPortfolioAllocator = ({
|
|
838
827
|
assets,
|
|
839
828
|
maxVolatility,
|
|
840
829
|
covarianceMatrix,
|
|
841
830
|
}) => {
|
|
842
|
-
//
|
|
843
|
-
//
|
|
831
|
+
// Evaluates a portfolio weight vector.
|
|
832
|
+
// Since solvers minimize, we minimize NEGATIVE expected return.
|
|
844
833
|
return function portfolioFitness(weights) {
|
|
845
|
-
//
|
|
834
|
+
// Normalize weights to ensure total equals 1.0
|
|
846
835
|
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
|
|
847
|
-
if (totalWeight === 0) return Infinity; //
|
|
836
|
+
if (totalWeight === 0) return Infinity; // Prevent zero-division
|
|
848
837
|
const normalizedWeights = weights.map((w) => w / totalWeight);
|
|
849
838
|
|
|
850
839
|
let portfolioReturn = normalizedWeights.reduce(
|
|
@@ -854,8 +843,7 @@ Optimization.Operators.createPortfolioAllocator = ({
|
|
|
854
843
|
let portfolioVolatility;
|
|
855
844
|
|
|
856
845
|
if (covarianceMatrix) {
|
|
857
|
-
//
|
|
858
|
-
// Volatilité^2 = w' * C * w
|
|
846
|
+
// Accurate covariance volatility: Volatility^2 = w' * C * w
|
|
859
847
|
let variance = 0;
|
|
860
848
|
for (let i = 0; i < assets.length; i++) {
|
|
861
849
|
for (let j = 0; j < assets.length; j++) {
|
|
@@ -867,7 +855,7 @@ Optimization.Operators.createPortfolioAllocator = ({
|
|
|
867
855
|
}
|
|
868
856
|
portfolioVolatility = Math.sqrt(variance);
|
|
869
857
|
} else {
|
|
870
|
-
//
|
|
858
|
+
// Simplified weighted average volatility
|
|
871
859
|
portfolioVolatility = normalizedWeights.reduce(
|
|
872
860
|
(sum, w, i) => sum + w * assets[i].volatility,
|
|
873
861
|
0,
|
|
@@ -876,39 +864,38 @@ Optimization.Operators.createPortfolioAllocator = ({
|
|
|
876
864
|
|
|
877
865
|
for (let i = 0; i < assets.length; i++) {}
|
|
878
866
|
|
|
879
|
-
//
|
|
867
|
+
// High penalty if risk ceiling is violated
|
|
880
868
|
if (portfolioVolatility > maxVolatility) {
|
|
881
|
-
return 1000 + (portfolioVolatility - maxVolatility) * 1000; //
|
|
869
|
+
return 1000 + (portfolioVolatility - maxVolatility) * 1000; // Proportional penalty
|
|
882
870
|
}
|
|
883
871
|
|
|
884
|
-
//
|
|
872
|
+
// Maximize return <=> minimize negative return
|
|
885
873
|
return -portfolioReturn;
|
|
886
874
|
};
|
|
887
875
|
};
|
|
888
876
|
|
|
889
877
|
/**
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
* @param {
|
|
893
|
-
* @
|
|
894
|
-
* @returns {{solution: Array<number>, energy: number}} Le chemin optimal (indices des villes) et sa distance.
|
|
878
|
+
* Solves the Traveling Salesperson Problem (TSP) using Simulated Annealing.
|
|
879
|
+
* @param {Array<{x: number, y: number}>} cities - City coordinates array.
|
|
880
|
+
* @param {object} [options] - Simulated Annealing parameters.
|
|
881
|
+
* @returns {{solution: Array<number>, energy: number}} Optimal tour (city indices) and total distance.
|
|
895
882
|
*/
|
|
896
883
|
Optimization.Operators.solveTSP = (cities, options = {}) => {
|
|
897
|
-
//
|
|
884
|
+
// Euclidean distance between two cities
|
|
898
885
|
const distance = (city1, city2) =>
|
|
899
886
|
Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
|
|
900
887
|
|
|
901
|
-
//
|
|
888
|
+
// Total route tour distance evaluator
|
|
902
889
|
const pathEvaluator = (path) => {
|
|
903
890
|
let totalDistance = 0;
|
|
904
891
|
for (let i = 0; i < path.length - 1; i++) {
|
|
905
892
|
totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
|
|
906
893
|
}
|
|
907
|
-
totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); //
|
|
894
|
+
totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Return to origin
|
|
908
895
|
return totalDistance;
|
|
909
896
|
};
|
|
910
897
|
|
|
911
|
-
//
|
|
898
|
+
// Neighborhood: inverts a random sub-sequence (2-opt heuristic)
|
|
912
899
|
const pathNeighbor = (path) => {
|
|
913
900
|
const newPath = [...path];
|
|
914
901
|
if (newPath.length <= 1) return newPath;
|
|
@@ -924,12 +911,12 @@ Optimization.Operators.solveTSP = (cities, options = {}) => {
|
|
|
924
911
|
return newPath;
|
|
925
912
|
};
|
|
926
913
|
|
|
927
|
-
//
|
|
914
|
+
// Initial solution: randomized route
|
|
928
915
|
const initialPath = Array.from({ length: cities.length }, (_, i) => i).sort(
|
|
929
916
|
() => random() - 0.5,
|
|
930
917
|
);
|
|
931
918
|
|
|
932
|
-
//
|
|
919
|
+
// Default hyperparameters for TSP
|
|
933
920
|
const saOptions = {
|
|
934
921
|
initialTemperature: 10000,
|
|
935
922
|
coolingRate: 0.999,
|
|
@@ -948,35 +935,35 @@ Optimization.Operators.solveTSP = (cities, options = {}) => {
|
|
|
948
935
|
};
|
|
949
936
|
|
|
950
937
|
/**
|
|
951
|
-
*
|
|
952
|
-
* @param {Array<{name: string, expectedReturn: number, volatility: number}>} assets -
|
|
953
|
-
* @param {number} maxVolatility -
|
|
954
|
-
* @param {object} [options] -
|
|
955
|
-
* @returns {{solution: Array<number>, fitness: number}}
|
|
938
|
+
* Solves portfolio allocation using a Genetic Algorithm.
|
|
939
|
+
* @param {Array<{name: string, expectedReturn: number, volatility: number}>} assets - Available assets.
|
|
940
|
+
* @param {number} maxVolatility - Maximum allowed volatility constraint.
|
|
941
|
+
* @param {object} [options] - Genetic algorithm options.
|
|
942
|
+
* @returns {{solution: Array<number>, fitness: number}} Optimal weights and associated fitness.
|
|
956
943
|
*/
|
|
957
944
|
Optimization.Operators.solvePortfolio = (
|
|
958
945
|
assets,
|
|
959
946
|
maxVolatility,
|
|
960
947
|
options = {},
|
|
961
948
|
) => {
|
|
962
|
-
//
|
|
949
|
+
// Fitness function created from the allocator operator
|
|
963
950
|
const fitnessFunction = Optimization.Operators.createPortfolioAllocator({
|
|
964
951
|
assets,
|
|
965
952
|
maxVolatility,
|
|
966
953
|
covarianceMatrix: options.covarianceMatrix,
|
|
967
954
|
});
|
|
968
955
|
|
|
969
|
-
//
|
|
956
|
+
// Problem-specific genetic operators
|
|
970
957
|
const createIndividual = () =>
|
|
971
958
|
Array.from({ length: assets.length }, () => random());
|
|
972
959
|
|
|
973
|
-
const crossover = (p1, p2) => p1.map((w1, i) => (w1 + p2[i]) / 2); //
|
|
960
|
+
const crossover = (p1, p2) => p1.map((w1, i) => (w1 + p2[i]) / 2); // Arithmetic average
|
|
974
961
|
|
|
975
962
|
const mutate = (p) => {
|
|
976
963
|
const newP = [...p];
|
|
977
964
|
const i = crypto.randomInt(0, newP.length);
|
|
978
|
-
newP[i] += (random() - 0.5) * 0.2; //
|
|
979
|
-
newP[i] = Math.max(0, newP[i]); //
|
|
965
|
+
newP[i] += (random() - 0.5) * 0.2; // Gentle mutation
|
|
966
|
+
newP[i] = Math.max(0, newP[i]); // Weights cannot be negative
|
|
980
967
|
return newP;
|
|
981
968
|
};
|
|
982
969
|
|
|
@@ -996,57 +983,56 @@ Optimization.Operators.solvePortfolio = (
|
|
|
996
983
|
};
|
|
997
984
|
|
|
998
985
|
/**
|
|
999
|
-
*
|
|
1000
|
-
* @param {object} config -
|
|
1001
|
-
* @param {number} config.totalAdvertiserCredits -
|
|
1002
|
-
* @param {Array<{qualityScore: number}>} config.websites -
|
|
1003
|
-
* @returns {function(Array<number>): number}
|
|
986
|
+
* Creates a 2D evaluator to determine base commission and quality bonus factor to maximize platform revenue.
|
|
987
|
+
* @param {object} config - Configuration object.
|
|
988
|
+
* @param {number} config.totalAdvertiserCredits - Total advertiser credits.
|
|
989
|
+
* @param {Array<{qualityScore: number}>} config.websites - Array of websites with quality scores.
|
|
990
|
+
* @returns {function(Array<number>): number} Evaluator for `[baseCommission, bonusFactor]` returning NEGATIVE revenue.
|
|
1004
991
|
*/
|
|
1005
992
|
Optimization.Operators.createAdvancedPlatformRevenueEvaluator = ({
|
|
1006
993
|
totalAdvertiserCredits,
|
|
1007
994
|
websites,
|
|
1008
995
|
}) => {
|
|
1009
|
-
//
|
|
1010
|
-
// La demande est sensible à la qualité globale de l'inventaire publicitaire.
|
|
996
|
+
// Advertiser demand model (increases with inventory quality)
|
|
1011
997
|
const advertiserDemandModel = (averageSiteQuality) => {
|
|
1012
|
-
//
|
|
998
|
+
// Baseline demand tied to available credits
|
|
1013
999
|
const baseDemand = (totalAdvertiserCredits || 100) * 10;
|
|
1014
|
-
//
|
|
1000
|
+
// Demand scales with average inventory quality
|
|
1015
1001
|
return baseDemand * (1 + averageSiteQuality);
|
|
1016
1002
|
};
|
|
1017
1003
|
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1004
|
+
// Publisher / webmaster supply model
|
|
1005
|
+
// Each site's click supply scales with its effective payout rate
|
|
1020
1006
|
const webmasterSupplyModel = (baseCommission, bonusFactor) => {
|
|
1021
1007
|
let totalOfferedClicks = 0;
|
|
1022
|
-
const baseSupplyPerSite = 500; //
|
|
1008
|
+
const baseSupplyPerSite = 500; // Baseline potential clicks per site
|
|
1023
1009
|
|
|
1024
1010
|
for (const site of websites) {
|
|
1025
|
-
//
|
|
1011
|
+
// Effective commission is discounted for higher quality sites
|
|
1026
1012
|
const effectiveCommission = Math.max(
|
|
1027
1013
|
0,
|
|
1028
1014
|
baseCommission - site.qualityScore * bonusFactor,
|
|
1029
1015
|
);
|
|
1030
1016
|
const webmasterPayoutRate = 1 - effectiveCommission;
|
|
1031
1017
|
|
|
1032
|
-
//
|
|
1018
|
+
// Supply is proportional to payout rate
|
|
1033
1019
|
totalOfferedClicks += baseSupplyPerSite * webmasterPayoutRate;
|
|
1034
1020
|
}
|
|
1035
1021
|
return totalOfferedClicks;
|
|
1036
1022
|
};
|
|
1037
1023
|
|
|
1038
|
-
//
|
|
1024
|
+
// Objective function for optimization algorithms
|
|
1039
1025
|
return function revenueEvaluator(solution) {
|
|
1040
1026
|
const [baseCommission, bonusFactor] = solution;
|
|
1041
1027
|
|
|
1042
|
-
//
|
|
1028
|
+
// Constraints: heavily penalize out-of-bound solutions
|
|
1043
1029
|
if (
|
|
1044
1030
|
baseCommission < 0.01 ||
|
|
1045
1031
|
baseCommission > 0.8 ||
|
|
1046
1032
|
bonusFactor < 0 ||
|
|
1047
1033
|
bonusFactor > baseCommission
|
|
1048
1034
|
) {
|
|
1049
|
-
return Infinity;
|
|
1035
|
+
return Infinity;
|
|
1050
1036
|
}
|
|
1051
1037
|
|
|
1052
1038
|
const averageQuality =
|
|
@@ -1064,19 +1050,19 @@ Optimization.Operators.createAdvancedPlatformRevenueEvaluator = ({
|
|
|
1064
1050
|
baseCommission - averageQuality * bonusFactor,
|
|
1065
1051
|
);
|
|
1066
1052
|
|
|
1067
|
-
//
|
|
1053
|
+
// Maximize revenue <=> minimize negative revenue
|
|
1068
1054
|
return -(clicks * averageCommission);
|
|
1069
1055
|
};
|
|
1070
1056
|
};
|
|
1071
1057
|
|
|
1072
1058
|
/**
|
|
1073
|
-
*
|
|
1074
|
-
* @param {Array<{x: number, y: number}>} customers -
|
|
1075
|
-
* @param {number} numFacilities -
|
|
1076
|
-
* @param {{minX: number, maxX: number, minY: number, maxY: number}} bounds -
|
|
1077
|
-
* @param {number} [options.fixedCostPerFacility=0] -
|
|
1078
|
-
* @param {object} [options] -
|
|
1079
|
-
* @returns {{solution: Array<{x: number, y: number}>, energy: number}}
|
|
1059
|
+
* Solves the Facility Location Problem using Simulated Annealing.
|
|
1060
|
+
* @param {Array<{x: number, y: number}>} customers - Customer coordinate pairs.
|
|
1061
|
+
* @param {number} numFacilities - Number of facilities to place.
|
|
1062
|
+
* @param {{minX: number, maxX: number, minY: number, maxY: number}} bounds - Boundary box for placing facilities.
|
|
1063
|
+
* @param {number} [options.fixedCostPerFacility=0] - Fixed installation cost per facility.
|
|
1064
|
+
* @param {object} [options] - Simulated Annealing options.
|
|
1065
|
+
* @returns {{solution: Array<{x: number, y: number}>, energy: number}} Optimal facility locations and total cost.
|
|
1080
1066
|
*/
|
|
1081
1067
|
Optimization.Operators.solveFacilityLocation = (
|
|
1082
1068
|
customers,
|
|
@@ -1086,9 +1072,9 @@ Optimization.Operators.solveFacilityLocation = (
|
|
|
1086
1072
|
) => {
|
|
1087
1073
|
const fixedCostPerFacility = options.fixedCostPerFacility || 0;
|
|
1088
1074
|
const distanceSq = (p1, p2) =>
|
|
1089
|
-
Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2); //
|
|
1075
|
+
Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2); // Squared Euclidean distance for efficiency
|
|
1090
1076
|
|
|
1091
|
-
//
|
|
1077
|
+
// Evaluator: computes sum of shortest distances from each customer to nearest facility
|
|
1092
1078
|
const facilityEvaluator = (facilities) => {
|
|
1093
1079
|
let totalConnectionCost = 0;
|
|
1094
1080
|
for (const customer of customers) {
|
|
@@ -1099,13 +1085,13 @@ Optimization.Operators.solveFacilityLocation = (
|
|
|
1099
1085
|
minDistanceToCustomer = d;
|
|
1100
1086
|
}
|
|
1101
1087
|
}
|
|
1102
|
-
totalConnectionCost += Math.sqrt(minDistanceToCustomer); //
|
|
1088
|
+
totalConnectionCost += Math.sqrt(minDistanceToCustomer); // True Euclidean distance for cost
|
|
1103
1089
|
}
|
|
1104
|
-
//
|
|
1090
|
+
// Total cost = connection cost + fixed facility maintenance costs
|
|
1105
1091
|
return totalConnectionCost + facilities.length * fixedCostPerFacility;
|
|
1106
1092
|
};
|
|
1107
1093
|
|
|
1108
|
-
//
|
|
1094
|
+
// Neighborhood: randomly perturb one facility position
|
|
1109
1095
|
const facilityNeighbor = (facilities) => {
|
|
1110
1096
|
const newFacilities = facilities.map((f) => ({ ...f }));
|
|
1111
1097
|
const i = crypto.randomInt(0, numFacilities);
|
|
@@ -1124,7 +1110,7 @@ Optimization.Operators.solveFacilityLocation = (
|
|
|
1124
1110
|
return newFacilities;
|
|
1125
1111
|
};
|
|
1126
1112
|
|
|
1127
|
-
//
|
|
1113
|
+
// Initial solution: distribute facilities randomly across bounds
|
|
1128
1114
|
const initialFacilities = Array.from({ length: numFacilities }, () => ({
|
|
1129
1115
|
x: bounds.minX + random() * (bounds.maxX - bounds.minX),
|
|
1130
1116
|
y: bounds.minY + random() * (bounds.maxY - bounds.minY),
|
|
@@ -1149,65 +1135,59 @@ Optimization.Operators.solveFacilityLocation = (
|
|
|
1149
1135
|
return result;
|
|
1150
1136
|
};
|
|
1151
1137
|
|
|
1152
|
-
//
|
|
1153
|
-
// 1 PRIM'S = 1 clic
|
|
1138
|
+
// Baseline click cost: 1 credit = 1 click
|
|
1154
1139
|
const BASE_CLICK_COST = 1;
|
|
1155
1140
|
/**
|
|
1156
|
-
*
|
|
1157
|
-
* @param {object}
|
|
1158
|
-
* @param {object}
|
|
1159
|
-
* @param {object}
|
|
1160
|
-
* @param {Array<object>}
|
|
1161
|
-
* @param {object}
|
|
1162
|
-
* @param {object}
|
|
1163
|
-
* @param {number}
|
|
1141
|
+
* Creates a multi-objective evaluator to determine optimal Cost Per Click (CPC).
|
|
1142
|
+
* @param {object} context - Configuration context.
|
|
1143
|
+
* @param {object} context.advertiser - Advertiser paying for the click.
|
|
1144
|
+
* @param {object} context.ad - Ad details.
|
|
1145
|
+
* @param {Array<object>} context.competingAds - Competing ads targeting the same keywords.
|
|
1146
|
+
* @param {object} context.website - Publisher website hosting the ad.
|
|
1147
|
+
* @param {object} context.platformParams - Platform commission parameters.
|
|
1148
|
+
* @param {number} context.estimatedImpressions - Estimated daily impressions.
|
|
1164
1149
|
*/
|
|
1165
1150
|
Optimization.Operators.createOptimalCPCEvaluator = (context) => {
|
|
1166
1151
|
const { optimalBaseCommission, optimalBonusFactor } = context.platformParams;
|
|
1167
1152
|
const websiteQualityScore = (context.website?.relevanceScore || 50) / 100;
|
|
1168
1153
|
|
|
1169
|
-
//
|
|
1154
|
+
// Effective commission rate for this publisher
|
|
1170
1155
|
const effectiveCommissionRate = Math.max(
|
|
1171
1156
|
0,
|
|
1172
1157
|
optimalBaseCommission - websiteQualityScore * optimalBonusFactor,
|
|
1173
1158
|
);
|
|
1174
1159
|
|
|
1175
|
-
//
|
|
1160
|
+
// Demand model: how many clicks can the advertiser afford?
|
|
1176
1161
|
const advertiserDemand = (cpc) => {
|
|
1177
1162
|
if (cpc <= 0) return Infinity;
|
|
1178
1163
|
return (context.advertiser.credits || 0) / cpc;
|
|
1179
1164
|
};
|
|
1180
1165
|
|
|
1181
|
-
//
|
|
1182
|
-
|
|
1183
|
-
const supply = context.estimatedImpressions || 1; // Fallback à 1 pour éviter la division par zéro.
|
|
1166
|
+
// Available inventory supply
|
|
1167
|
+
const supply = context.estimatedImpressions || 1; // Fallback to 1 to prevent zero-division
|
|
1184
1168
|
|
|
1185
|
-
//
|
|
1186
|
-
// Formule simple : 1 + (0.1 * nombre de concurrents), avec un plafond.
|
|
1169
|
+
// Competition factor scales price based on competition density (capped at 2.5)
|
|
1187
1170
|
const competitionFactor = Math.min(
|
|
1188
1171
|
2.5,
|
|
1189
1172
|
1 + context.competingAds.length * 0.1,
|
|
1190
1173
|
);
|
|
1191
1174
|
|
|
1192
1175
|
return function cpcFitness(cpcMultiplier) {
|
|
1193
|
-
//
|
|
1176
|
+
// Final CPC is baseline cost adjusted by optimizer multiplier and competition factor
|
|
1194
1177
|
const adjustedCPC = BASE_CLICK_COST * cpcMultiplier * competitionFactor;
|
|
1195
|
-
if (adjustedCPC < 0.1) return [Infinity, Infinity, Infinity]; // CPC
|
|
1178
|
+
if (adjustedCPC < 0.1) return [Infinity, Infinity, Infinity]; // Minimum CPC constraint
|
|
1196
1179
|
|
|
1197
|
-
// La demande de l'annonceur est calculée avec le CPC ajusté.
|
|
1198
1180
|
const demand = advertiserDemand(adjustedCPC);
|
|
1199
|
-
|
|
1200
|
-
// Le nombre de clics est le minimum de l'offre et de la demande.
|
|
1201
1181
|
const estimatedClicks = Math.min(demand, supply);
|
|
1202
1182
|
|
|
1203
|
-
//
|
|
1183
|
+
// Objective 1: Maximize platform revenue (minimize negative revenue)
|
|
1204
1184
|
const platformRevenue =
|
|
1205
1185
|
estimatedClicks * adjustedCPC * effectiveCommissionRate;
|
|
1206
1186
|
|
|
1207
|
-
//
|
|
1187
|
+
// Objective 2: Maximize advertiser value (clicks delivered, minimize negative)
|
|
1208
1188
|
const advertiserValue = estimatedClicks;
|
|
1209
1189
|
|
|
1210
|
-
//
|
|
1190
|
+
// Objective 3: Minimize market imbalance (supply vs demand)
|
|
1211
1191
|
const marketImbalance = Math.abs(demand - supply);
|
|
1212
1192
|
|
|
1213
1193
|
return [-platformRevenue, -advertiserValue, marketImbalance];
|
|
@@ -1215,47 +1195,44 @@ Optimization.Operators.createOptimalCPCEvaluator = (context) => {
|
|
|
1215
1195
|
};
|
|
1216
1196
|
|
|
1217
1197
|
/**
|
|
1218
|
-
*
|
|
1198
|
+
* Applies competition factor to baseline CPC.
|
|
1219
1199
|
* @private
|
|
1220
|
-
* @param {number} baseCpc -
|
|
1221
|
-
* @param {Array<object>} competingAds -
|
|
1222
|
-
* @returns {number}
|
|
1200
|
+
* @param {number} baseCpc - Baseline CPC from genetic algorithm.
|
|
1201
|
+
* @param {Array<object>} competingAds - Competing ads.
|
|
1202
|
+
* @returns {number} Final adjusted CPC.
|
|
1223
1203
|
*/
|
|
1224
1204
|
function applyCompetitionFactor(baseCpc, competingAds) {
|
|
1225
|
-
// Formule simple : 1 + (0.1 * nombre de concurrents), avec un plafond pour éviter l'explosion des prix.
|
|
1226
1205
|
const competitionFactor = Math.min(
|
|
1227
1206
|
2.5,
|
|
1228
1207
|
1 + (competingAds || []).length * 0.1,
|
|
1229
1208
|
);
|
|
1230
1209
|
const finalCpc = baseCpc * competitionFactor;
|
|
1231
|
-
// On s'assure de ne jamais descendre sous un seuil minimal.
|
|
1232
1210
|
return Math.max(0.1, finalCpc);
|
|
1233
1211
|
}
|
|
1234
1212
|
/**
|
|
1235
|
-
*
|
|
1236
|
-
* @param {object} context -
|
|
1237
|
-
* @param {object} [options] -
|
|
1238
|
-
* @returns {Array<{solution: number, objectives: number[]}>}
|
|
1213
|
+
* Solves optimal CPC configuration using a multi-objective genetic algorithm.
|
|
1214
|
+
* @param {object} context - Context required for evaluation (advertiser, ad, etc.).
|
|
1215
|
+
* @param {object} [options] - Genetic algorithm options.
|
|
1216
|
+
* @returns {Array<{solution: number, objectives: number[]}>} Pareto front of optimal CPC solutions.
|
|
1239
1217
|
*/
|
|
1240
1218
|
Optimization.Operators.solveOptimalCPC = (context, options = {}) => {
|
|
1241
1219
|
const fitnessFunction =
|
|
1242
1220
|
Optimization.Operators.createOptimalCPCEvaluator(context);
|
|
1243
1221
|
|
|
1244
|
-
//
|
|
1222
|
+
// An individual is a CPC multiplier scalar
|
|
1245
1223
|
const createIndividual = () => {
|
|
1246
|
-
// Le CPC peut varier, par exemple, entre 0.5 et 5 PRIM'S.
|
|
1247
1224
|
return 0.5 + random() * 4.5;
|
|
1248
1225
|
};
|
|
1249
1226
|
|
|
1250
|
-
//
|
|
1227
|
+
// Crossover: average of parent multipliers
|
|
1251
1228
|
const crossover = (cpc1, cpc2) => {
|
|
1252
1229
|
return (cpc1 + cpc2) / 2;
|
|
1253
1230
|
};
|
|
1254
1231
|
|
|
1255
|
-
// Mutation
|
|
1232
|
+
// Mutation: slight random variation
|
|
1256
1233
|
const mutate = (cpc) => {
|
|
1257
1234
|
const newCpc = cpc + (random() - 0.5) * 0.5;
|
|
1258
|
-
return Math.max(0.1, newCpc);
|
|
1235
|
+
return Math.max(0.1, newCpc);
|
|
1259
1236
|
};
|
|
1260
1237
|
|
|
1261
1238
|
const gaOptions = {
|
|
@@ -1264,7 +1241,6 @@ Optimization.Operators.solveOptimalCPC = (context, options = {}) => {
|
|
|
1264
1241
|
...options,
|
|
1265
1242
|
};
|
|
1266
1243
|
|
|
1267
|
-
// On utilise l'algorithme génétique multi-objectifs pour obtenir le front de Pareto.
|
|
1268
1244
|
const paretoFront = Optimization.geneticAlgorithmMultiObjective(
|
|
1269
1245
|
createIndividual,
|
|
1270
1246
|
fitnessFunction,
|
|
@@ -1273,60 +1249,53 @@ Optimization.Operators.solveOptimalCPC = (context, options = {}) => {
|
|
|
1273
1249
|
gaOptions,
|
|
1274
1250
|
);
|
|
1275
1251
|
|
|
1276
|
-
//
|
|
1252
|
+
// Apply competition factor to final Pareto solutions
|
|
1277
1253
|
return paretoFront.map((result) => {
|
|
1278
1254
|
const cpcMultiplier = result.solution;
|
|
1279
|
-
// Le CPC final est calculé ici, en dehors de la fonction de fitness.
|
|
1280
1255
|
const finalCpc = applyCompetitionFactor(
|
|
1281
1256
|
BASE_CLICK_COST * cpcMultiplier,
|
|
1282
1257
|
context.competingAds,
|
|
1283
1258
|
);
|
|
1284
1259
|
return {
|
|
1285
1260
|
...result,
|
|
1286
|
-
solution: Math.max(0.1, finalCpc),
|
|
1261
|
+
solution: Math.max(0.1, finalCpc),
|
|
1287
1262
|
};
|
|
1288
1263
|
});
|
|
1289
1264
|
};
|
|
1290
1265
|
|
|
1291
1266
|
/**
|
|
1292
|
-
*
|
|
1293
|
-
* @param {object} context -
|
|
1294
|
-
* @param {number} context.suspicionScore -
|
|
1295
|
-
* @returns {function(number): number[]}
|
|
1267
|
+
* Creates a multi-objective evaluator to find optimal TTL (Time-To-Live) for a security ticket.
|
|
1268
|
+
* @param {object} context - Configuration context.
|
|
1269
|
+
* @param {number} context.suspicionScore - User suspicion score (0-100).
|
|
1270
|
+
* @returns {function(number): number[]} Fitness function returning [risk, friction] objective scores.
|
|
1296
1271
|
*/
|
|
1297
1272
|
Optimization.Operators.createOptimalTtlEvaluator = ({ suspicionScore }) => {
|
|
1298
|
-
//
|
|
1273
|
+
// Normalize score to ensure strong weight in risk calculation
|
|
1299
1274
|
const normalizedScore = Math.max(1, suspicionScore);
|
|
1300
1275
|
|
|
1301
1276
|
return function ttlFitness(ttl) {
|
|
1302
|
-
//
|
|
1277
|
+
// Constraints: TTL must be within realistic bounds (5m to 24h)
|
|
1303
1278
|
if (ttl < 300000 || ttl > 86400000) return [Infinity, Infinity];
|
|
1304
1279
|
|
|
1305
|
-
//
|
|
1306
|
-
// Le risque est le produit du score et de la durée de la session.
|
|
1307
|
-
// Pour un score élevé, l'algo doit choisir un TTL faible pour minimiser ce produit.
|
|
1280
|
+
// Objective 1: Minimize Security Risk (product of suspicion score and session duration)
|
|
1308
1281
|
const risk = normalizedScore * ttl;
|
|
1309
1282
|
|
|
1310
|
-
//
|
|
1311
|
-
// La friction est l'inverse du TTL. On la pénalise d'autant plus que le score est FAIBLE.
|
|
1312
|
-
// (101 - score) assure que pour un score de 1, la pénalité d'un TTL court est maximale.
|
|
1313
|
-
// Pour un score de 100, cette pénalité est quasi nulle.
|
|
1283
|
+
// Objective 2: Minimize UX Friction (inverse of TTL, penalized when suspicion is low)
|
|
1314
1284
|
const friction = (1 / ttl) * (101 - normalizedScore);
|
|
1315
1285
|
|
|
1316
|
-
//
|
|
1286
|
+
// Return scaled objectives to balance dimensions
|
|
1317
1287
|
return [risk / 1e7, friction * 1e9];
|
|
1318
1288
|
};
|
|
1319
1289
|
};
|
|
1320
1290
|
|
|
1321
1291
|
/**
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1324
|
-
* @param {string}
|
|
1325
|
-
* @returns {number}
|
|
1292
|
+
* Calculates Benford's Law deviation for a series of numbers.
|
|
1293
|
+
* Elevated scores indicate unnatural/synthetic data distributions.
|
|
1294
|
+
* @param {Array<number|string>} numbers - Array of numeric samples.
|
|
1295
|
+
* @returns {number} Deviation score (0 = perfect match, > 0.15 = suspicious).
|
|
1326
1296
|
*/
|
|
1327
1297
|
Optimization.Operators.benfordTest = (numbers) => {
|
|
1328
1298
|
if (!Array.isArray(numbers)) {
|
|
1329
|
-
// Si l'entrée n'est pas un tableau, on ne peut pas l'analyser.
|
|
1330
1299
|
return 0;
|
|
1331
1300
|
}
|
|
1332
1301
|
const counts = Array(10).fill(0);
|
|
@@ -1354,12 +1323,12 @@ Optimization.Operators.benfordTest = (numbers) => {
|
|
|
1354
1323
|
}
|
|
1355
1324
|
|
|
1356
1325
|
if (validCount < 10) {
|
|
1357
|
-
return 0; //
|
|
1326
|
+
return 0; // Insufficient data points for reliable statistical testing
|
|
1358
1327
|
}
|
|
1359
1328
|
|
|
1360
|
-
//
|
|
1329
|
+
// Expected Benford's Law distribution for leading digits 1 through 9
|
|
1361
1330
|
const benfordDistribution = [
|
|
1362
|
-
0, // Index 0
|
|
1331
|
+
0, // Index 0 unused
|
|
1363
1332
|
30.1, 17.6, 12.5, 9.7, 7.9, 6.7, 5.8, 5.1, 4.6
|
|
1364
1333
|
];
|
|
1365
1334
|
|
|
@@ -1370,8 +1339,7 @@ Optimization.Operators.benfordTest = (numbers) => {
|
|
|
1370
1339
|
totalDeviation += Math.pow(observedFrequency - expectedFrequency, 2);
|
|
1371
1340
|
}
|
|
1372
1341
|
|
|
1373
|
-
//
|
|
1374
|
-
// Cette normalisation est empirique.
|
|
1342
|
+
// Normalize deviation into an interpretable metric scale
|
|
1375
1343
|
return Math.sqrt(totalDeviation) / 50;
|
|
1376
1344
|
};
|
|
1377
1345
|
|
|
@@ -1379,17 +1347,17 @@ Optimization.Operators.benfordTest = (numbers) => {
|
|
|
1379
1347
|
|
|
1380
1348
|
|
|
1381
1349
|
/**
|
|
1382
|
-
*
|
|
1383
|
-
* @param {object} config -
|
|
1384
|
-
* @param {Array<object>} config.legitimateClicks -
|
|
1385
|
-
* @param {Array<object>} config.fraudulentClicks -
|
|
1386
|
-
* @returns {function(Array<number>): number[]}
|
|
1350
|
+
* Creates a multi-objective evaluator to identify optimal fraud detection thresholds.
|
|
1351
|
+
* @param {object} config - Configuration object.
|
|
1352
|
+
* @param {Array<object>} config.legitimateClicks - Sample of legitimate click events.
|
|
1353
|
+
* @param {Array<object>} config.fraudulentClicks - Sample of identified fraudulent clicks (e.g. honeypots).
|
|
1354
|
+
* @returns {function(Array<number>): number[]} Fitness function returning [1 - TPR, FPR].
|
|
1387
1355
|
*/
|
|
1388
1356
|
Optimization.Operators.createFraudThresholdEvaluator = ({
|
|
1389
1357
|
legitimateClicks,
|
|
1390
1358
|
fraudulentClicks,
|
|
1391
1359
|
}) => {
|
|
1392
|
-
//
|
|
1360
|
+
// Helper to compute click coordinate variance for a given fingerprint
|
|
1393
1361
|
const calculateClickVariance = (clicks) => {
|
|
1394
1362
|
if (!clicks || clicks.length < 2) return 0;
|
|
1395
1363
|
const meanX = clicks.reduce((sum, c) => sum + c.clickX, 0) / clicks.length;
|
|
@@ -1403,7 +1371,7 @@ Optimization.Operators.createFraudThresholdEvaluator = ({
|
|
|
1403
1371
|
return variance;
|
|
1404
1372
|
};
|
|
1405
1373
|
|
|
1406
|
-
//
|
|
1374
|
+
// Pre-group click events by fingerprint
|
|
1407
1375
|
const getClicksByFingerprint = (clickData) => {
|
|
1408
1376
|
const grouped = {};
|
|
1409
1377
|
for (const click of clickData) {
|
|
@@ -1420,7 +1388,7 @@ Optimization.Operators.createFraudThresholdEvaluator = ({
|
|
|
1420
1388
|
const [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents] =
|
|
1421
1389
|
solution;
|
|
1422
1390
|
|
|
1423
|
-
//
|
|
1391
|
+
// Logical threshold boundary constraints
|
|
1424
1392
|
if (
|
|
1425
1393
|
minTimeToClick < 100 ||
|
|
1426
1394
|
minTimeToClick > 5000 ||
|
|
@@ -1430,18 +1398,17 @@ Optimization.Operators.createFraudThresholdEvaluator = ({
|
|
|
1430
1398
|
minMouseEntropy > 1 ||
|
|
1431
1399
|
minScrollEvents < 0
|
|
1432
1400
|
) {
|
|
1433
|
-
return [Infinity, Infinity];
|
|
1401
|
+
return [Infinity, Infinity];
|
|
1434
1402
|
}
|
|
1435
1403
|
|
|
1436
|
-
let truePositives = 0; // Bots
|
|
1437
|
-
let falsePositives = 0; //
|
|
1404
|
+
let truePositives = 0; // Bots accurately detected
|
|
1405
|
+
let falsePositives = 0; // Humans falsely flagged
|
|
1438
1406
|
|
|
1439
|
-
//
|
|
1407
|
+
// Evaluate fraudulent clicks
|
|
1440
1408
|
for (const fingerprint in fraudulentGroups) {
|
|
1441
1409
|
const clicks = fraudulentGroups[fingerprint];
|
|
1442
1410
|
if (!clicks) continue;
|
|
1443
1411
|
const variance = calculateClickVariance(clicks);
|
|
1444
|
-
// Un groupe de clics est frauduleux si l'une des conditions est remplie
|
|
1445
1412
|
const isTooFast = clicks.some((c) => c.timeToClick < minTimeToClick);
|
|
1446
1413
|
const isTooUniform = variance < maxClickVariance;
|
|
1447
1414
|
const hasLowEntropy = clicks.some(
|
|
@@ -1456,7 +1423,7 @@ Optimization.Operators.createFraudThresholdEvaluator = ({
|
|
|
1456
1423
|
}
|
|
1457
1424
|
}
|
|
1458
1425
|
|
|
1459
|
-
//
|
|
1426
|
+
// Evaluate legitimate clicks
|
|
1460
1427
|
for (const fingerprint in legitimateGroups) {
|
|
1461
1428
|
const clicks = legitimateGroups[fingerprint];
|
|
1462
1429
|
if (!clicks) continue;
|
|
@@ -1477,10 +1444,10 @@ Optimization.Operators.createFraudThresholdEvaluator = ({
|
|
|
1477
1444
|
const totalFraudulent = Object.keys(fraudulentGroups).length || 1;
|
|
1478
1445
|
const totalLegitimate = Object.keys(legitimateGroups).length || 1;
|
|
1479
1446
|
|
|
1480
|
-
//
|
|
1447
|
+
// Objective 1: Maximize fraud detection rate (minimize 1 - TPR)
|
|
1481
1448
|
const objective1 = 1 - truePositives / totalFraudulent;
|
|
1482
1449
|
|
|
1483
|
-
//
|
|
1450
|
+
// Objective 2: Minimize false positive rate (minimize FPR)
|
|
1484
1451
|
const objective2 = falsePositives / totalLegitimate;
|
|
1485
1452
|
|
|
1486
1453
|
return [objective1, objective2];
|
|
@@ -1488,27 +1455,27 @@ Optimization.Operators.createFraudThresholdEvaluator = ({
|
|
|
1488
1455
|
};
|
|
1489
1456
|
|
|
1490
1457
|
/**
|
|
1491
|
-
*
|
|
1492
|
-
* @param {object} context -
|
|
1493
|
-
* @param {Array<object>} context.legitimateClicks -
|
|
1494
|
-
* @param {Array<object>} context.fraudulentClicks -
|
|
1495
|
-
* @param {object} [options] -
|
|
1496
|
-
* @returns {Array<{solution: Array<number>, objectives: number[]}>}
|
|
1458
|
+
* Solves fraud detection optimization by finding the Pareto front of threshold values.
|
|
1459
|
+
* @param {object} context - Context containing click datasets.
|
|
1460
|
+
* @param {Array<object>} context.legitimateClicks - Sample of legitimate clicks.
|
|
1461
|
+
* @param {Array<object>} context.fraudulentClicks - Sample of fraudulent clicks.
|
|
1462
|
+
* @param {object} [options] - Genetic algorithm options.
|
|
1463
|
+
* @returns {Array<{solution: Array<number>, objectives: number[]}>} Pareto front of [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents].
|
|
1497
1464
|
*/
|
|
1498
1465
|
Optimization.Operators.solveFraudDetection = (context, options = {}) => {
|
|
1499
1466
|
const fitnessFunction =
|
|
1500
1467
|
Optimization.Operators.createFraudThresholdEvaluator(context);
|
|
1501
1468
|
|
|
1502
|
-
//
|
|
1469
|
+
// An individual is a 4-threshold array: [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]
|
|
1503
1470
|
const createIndividual = () => {
|
|
1504
|
-
const minTimeToClick = 100 + random() * 4900; //
|
|
1505
|
-
const maxClickVariance = 1 + random() * 9999; //
|
|
1506
|
-
const minMouseEntropy = random() * 0.5; //
|
|
1507
|
-
const minScrollEvents = crypto.randomInt(0, 10); //
|
|
1471
|
+
const minTimeToClick = 100 + random() * 4900; // between 100ms and 5s
|
|
1472
|
+
const maxClickVariance = 1 + random() * 9999; // between 1 and 10000
|
|
1473
|
+
const minMouseEntropy = random() * 0.5; // between 0 and 0.5
|
|
1474
|
+
const minScrollEvents = crypto.randomInt(0, 10); // between 0 and 10
|
|
1508
1475
|
return [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents];
|
|
1509
1476
|
};
|
|
1510
1477
|
|
|
1511
|
-
//
|
|
1478
|
+
// Crossover: arithmetic mean of parent thresholds
|
|
1512
1479
|
const crossover = (s1, s2) => {
|
|
1513
1480
|
return [
|
|
1514
1481
|
(s1[0] + s2[0]) / 2,
|
|
@@ -1518,14 +1485,14 @@ Optimization.Operators.solveFraudDetection = (context, options = {}) => {
|
|
|
1518
1485
|
];
|
|
1519
1486
|
};
|
|
1520
1487
|
|
|
1521
|
-
// Mutation
|
|
1488
|
+
// Mutation: slight random perturbation of a single threshold
|
|
1522
1489
|
const mutate = (solution) => {
|
|
1523
1490
|
const newSolution = [...solution];
|
|
1524
1491
|
const i = crypto.randomInt(0, 4);
|
|
1525
|
-
//
|
|
1492
|
+
// Specific mutation step scales for each dimension
|
|
1526
1493
|
const mutationFactors = [500, 1000, 0.1, 2];
|
|
1527
1494
|
const mutationFactor = mutationFactors[i];
|
|
1528
|
-
newSolution[i] += (
|
|
1495
|
+
newSolution[i] += (random() - 0.5) * mutationFactor;
|
|
1529
1496
|
return newSolution;
|
|
1530
1497
|
};
|
|
1531
1498
|
|
|
@@ -1545,11 +1512,11 @@ Optimization.Operators.solveFraudDetection = (context, options = {}) => {
|
|
|
1545
1512
|
};
|
|
1546
1513
|
|
|
1547
1514
|
/**
|
|
1548
|
-
*
|
|
1549
|
-
*
|
|
1550
|
-
* @param {object}
|
|
1551
|
-
* @param {Array<object>}
|
|
1552
|
-
* @returns {function(object): number[]}
|
|
1515
|
+
* Creates a multi-objective evaluator for end-to-end security configuration auto-tuning.
|
|
1516
|
+
* Jointly optimizes action thresholds, suspicion weights, and pattern detection parameters.
|
|
1517
|
+
* @param {object} context - Configuration context.
|
|
1518
|
+
* @param {Array<object>} context.trafficData - Collected traffic logs.
|
|
1519
|
+
* @returns {function(object): number[]} Fitness function returning [weighted FPR, weighted FNR].
|
|
1553
1520
|
*/
|
|
1554
1521
|
Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
1555
1522
|
const trafficData = context.trafficData || [];
|
|
@@ -1559,25 +1526,25 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1559
1526
|
const THREAT_PROFILES = {
|
|
1560
1527
|
account_takeover: {
|
|
1561
1528
|
importance: 10.0,
|
|
1562
|
-
ux_vs_security_ratio: 0.1, // 10% FPR / 90% FNR (
|
|
1529
|
+
ux_vs_security_ratio: 0.1, // 10% FPR / 90% FNR (Highest security priority)
|
|
1563
1530
|
target_threshold: 'block',
|
|
1564
1531
|
indicators: ['requestPatternScore', 'behaviorScore', 'timeInconsistencyScore', 'clickVarianceScore']
|
|
1565
1532
|
},
|
|
1566
1533
|
active_exploitation: {
|
|
1567
1534
|
importance: 8.0,
|
|
1568
|
-
ux_vs_security_ratio: 0.2, // 20% FPR / 80% FNR (
|
|
1535
|
+
ux_vs_security_ratio: 0.2, // 20% FPR / 80% FNR (Security prioritized)
|
|
1569
1536
|
target_threshold: 'block',
|
|
1570
1537
|
indicators: ['honeypotScore', 'headerAnomalyScore']
|
|
1571
1538
|
},
|
|
1572
1539
|
mass_scraping: {
|
|
1573
1540
|
importance: 3.0,
|
|
1574
|
-
ux_vs_security_ratio: 0.8, // 80% FPR / 20% FNR (UX
|
|
1541
|
+
ux_vs_security_ratio: 0.8, // 80% FPR / 20% FNR (UX prioritized)
|
|
1575
1542
|
target_threshold: 'low',
|
|
1576
|
-
indicators: ['requestPatternScore', 'renderingAnomalyScore', 'clientHintsInconsistencyScore']
|
|
1543
|
+
indicators: ['requestPatternScore', 'renderingAnomalyScore', 'clientHintsInconsistencyScore', 'virtualizationScore']
|
|
1577
1544
|
},
|
|
1578
1545
|
distributed_botnets: {
|
|
1579
1546
|
importance: 6.0,
|
|
1580
|
-
ux_vs_security_ratio: 0.5, //
|
|
1547
|
+
ux_vs_security_ratio: 0.5, // Balanced
|
|
1581
1548
|
target_threshold: 'high',
|
|
1582
1549
|
indicators: ['subnetScore', 'botnetClusterScore', 'ipReputationScore', 'tlsSpoofingScore']
|
|
1583
1550
|
},
|
|
@@ -1585,11 +1552,11 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1585
1552
|
importance: 5.0,
|
|
1586
1553
|
ux_vs_security_ratio: 0.4, // 40% FPR / 60% FNR
|
|
1587
1554
|
target_threshold: 'medium',
|
|
1588
|
-
indicators: ['botScore', 'tlsSpoofingScore', 'tcpAnomalyScore']
|
|
1555
|
+
indicators: ['botScore', 'tlsSpoofingScore', 'tcpAnomalyScore', 'virtualizationScore']
|
|
1589
1556
|
}
|
|
1590
1557
|
};
|
|
1591
1558
|
|
|
1592
|
-
//
|
|
1559
|
+
// Immutable baseline anchors to calibrate the suspicion scale
|
|
1593
1560
|
const STATIC_ANCHORS = [
|
|
1594
1561
|
{
|
|
1595
1562
|
type: 'request_passed',
|
|
@@ -1600,7 +1567,8 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1600
1567
|
crossLayerInconsistencyScore: 0, timeInconsistencyScore: 0, tlsSpoofingScore: 0,
|
|
1601
1568
|
clickVarianceScore: 0, clientHintsInconsistencyScore: 0, subnetScore: 0,
|
|
1602
1569
|
ipReputationScore: 0, botnetClusterScore: 0, tcpAnomalyScore: 0,
|
|
1603
|
-
quicAnomalyScore: 0, renderingAnomalyScore: 0, threatIntelScore: 0
|
|
1570
|
+
quicAnomalyScore: 0, renderingAnomalyScore: 0, threatIntelScore: 0,
|
|
1571
|
+
virtualizationScore: 0
|
|
1604
1572
|
}
|
|
1605
1573
|
},
|
|
1606
1574
|
{
|
|
@@ -1612,11 +1580,12 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1612
1580
|
crossLayerInconsistencyScore: 80, timeInconsistencyScore: 100, tlsSpoofingScore: 100,
|
|
1613
1581
|
clickVarianceScore: 100, clientHintsInconsistencyScore: 90, subnetScore: 100,
|
|
1614
1582
|
ipReputationScore: 100, botnetClusterScore: 100, tcpAnomalyScore: 100,
|
|
1615
|
-
quicAnomalyScore: 100, renderingAnomalyScore: 100, threatIntelScore: 100
|
|
1583
|
+
quicAnomalyScore: 100, renderingAnomalyScore: 100, threatIntelScore: 100,
|
|
1584
|
+
virtualizationScore: 100
|
|
1616
1585
|
}
|
|
1617
1586
|
},
|
|
1618
1587
|
{
|
|
1619
|
-
type: 'request_passed', //
|
|
1588
|
+
type: 'request_passed', // Clean human profile with minor noise (must remain below alert threshold, e.g. < 20)
|
|
1620
1589
|
weight: 10.0,
|
|
1621
1590
|
vector: {
|
|
1622
1591
|
historyScore: 15, rotationScore: 10, headerAnomalyScore: 20, requestPatternScore: 15,
|
|
@@ -1624,11 +1593,12 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1624
1593
|
crossLayerInconsistencyScore: 10, timeInconsistencyScore: 10, tlsSpoofingScore: 10,
|
|
1625
1594
|
clickVarianceScore: 0, clientHintsInconsistencyScore: 10, subnetScore: 5,
|
|
1626
1595
|
ipReputationScore: 5, botnetClusterScore: 5, tcpAnomalyScore: 10,
|
|
1627
|
-
quicAnomalyScore: 10, renderingAnomalyScore: 5, threatIntelScore: 0
|
|
1596
|
+
quicAnomalyScore: 10, renderingAnomalyScore: 5, threatIntelScore: 0,
|
|
1597
|
+
virtualizationScore: 0
|
|
1628
1598
|
}
|
|
1629
1599
|
},
|
|
1630
1600
|
{
|
|
1631
|
-
type: 'challenge_issued', //
|
|
1601
|
+
type: 'challenge_issued', // Stealth automated bot profile (must be challenged, e.g. > 35)
|
|
1632
1602
|
weight: 10.0,
|
|
1633
1603
|
vector: {
|
|
1634
1604
|
historyScore: 20, rotationScore: 20, headerAnomalyScore: 20, requestPatternScore: 40,
|
|
@@ -1636,7 +1606,8 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1636
1606
|
crossLayerInconsistencyScore: 20, timeInconsistencyScore: 20, tlsSpoofingScore: 30,
|
|
1637
1607
|
clickVarianceScore: 20, clientHintsInconsistencyScore: 20, subnetScore: 15,
|
|
1638
1608
|
ipReputationScore: 15, botnetClusterScore: 20, tcpAnomalyScore: 30,
|
|
1639
|
-
quicAnomalyScore: 20, renderingAnomalyScore: 20, threatIntelScore: 0
|
|
1609
|
+
quicAnomalyScore: 20, renderingAnomalyScore: 20, threatIntelScore: 0,
|
|
1610
|
+
virtualizationScore: 30
|
|
1640
1611
|
}
|
|
1641
1612
|
}
|
|
1642
1613
|
];
|
|
@@ -1666,7 +1637,7 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1666
1637
|
trap_triggered: 2.0,
|
|
1667
1638
|
};
|
|
1668
1639
|
|
|
1669
|
-
// 1.
|
|
1640
|
+
// 1. Evaluate on real traffic logs
|
|
1670
1641
|
for (const log of trafficData) {
|
|
1671
1642
|
const weight = log.weight || 1.0;
|
|
1672
1643
|
const confidence = (confidenceWeights[log.type] || 1.0) * weight;
|
|
@@ -1707,7 +1678,7 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1707
1678
|
}
|
|
1708
1679
|
|
|
1709
1680
|
}
|
|
1710
|
-
// 2.
|
|
1681
|
+
// 2. Evaluate against immutable baseline anchors to stabilize scale
|
|
1711
1682
|
for (const anchor of STATIC_ANCHORS) {
|
|
1712
1683
|
const score = calculateScore(anchor);
|
|
1713
1684
|
const weight = anchor.weight;
|
|
@@ -1734,12 +1705,12 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1734
1705
|
if (isBot) {
|
|
1735
1706
|
threatStats[threatName].totalBots += effectiveWeight;
|
|
1736
1707
|
if (score < targetThreshold) {
|
|
1737
|
-
threatStats[threatName].fn += effectiveWeight * 10; //
|
|
1708
|
+
threatStats[threatName].fn += effectiveWeight * 10; // Strict punitive penalty
|
|
1738
1709
|
}
|
|
1739
1710
|
} else {
|
|
1740
1711
|
threatStats[threatName].totalHumans += effectiveWeight;
|
|
1741
1712
|
if (score >= targetThreshold) {
|
|
1742
|
-
threatStats[threatName].fp += effectiveWeight * 10; //
|
|
1713
|
+
threatStats[threatName].fp += effectiveWeight * 10; // Strict punitive penalty
|
|
1743
1714
|
}
|
|
1744
1715
|
}
|
|
1745
1716
|
}
|
|
@@ -1767,7 +1738,7 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1767
1738
|
weightedFnr += fnr * importanceWeight * secRatio;
|
|
1768
1739
|
}
|
|
1769
1740
|
|
|
1770
|
-
// 3.
|
|
1741
|
+
// 3. L2 drift penalty against baseline configuration
|
|
1771
1742
|
let regularizationPenalty = 0;
|
|
1772
1743
|
if (currentConfig && currentConfig.weights) {
|
|
1773
1744
|
for (const key in config.weights) {
|
|
@@ -1776,7 +1747,7 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1776
1747
|
}
|
|
1777
1748
|
}
|
|
1778
1749
|
|
|
1779
|
-
// 4.
|
|
1750
|
+
// 4. Maximizing separation margin (SVM-like Margin Loss)
|
|
1780
1751
|
const marginOverlap = Math.max(0, maxHumanScore - minBotScore);
|
|
1781
1752
|
const marginPenalty = marginOverlap / 100;
|
|
1782
1753
|
|
|
@@ -1787,10 +1758,10 @@ Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
|
1787
1758
|
};
|
|
1788
1759
|
|
|
1789
1760
|
/**
|
|
1790
|
-
*
|
|
1791
|
-
* @param {object} context -
|
|
1792
|
-
* @param {object} [options] -
|
|
1793
|
-
* @returns {Array<{solution: object, objectives: number[]}>}
|
|
1761
|
+
* Solves the full security configuration auto-tuning problem.
|
|
1762
|
+
* @param {object} context - Context containing traffic event logs.
|
|
1763
|
+
* @param {object} [options] - Genetic algorithm options.
|
|
1764
|
+
* @returns {Array<{solution: object, objectives: number[]}>} Pareto front of optimal security configurations.
|
|
1794
1765
|
*/ // eslint-disable-line max-len
|
|
1795
1766
|
Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
1796
1767
|
const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context);
|
|
@@ -1813,10 +1784,11 @@ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
|
1813
1784
|
tcpAnomalyScore: 0.8,
|
|
1814
1785
|
quicAnomalyScore: 0.8,
|
|
1815
1786
|
renderingAnomalyScore: 0.8,
|
|
1816
|
-
threatIntelScore: 1.0
|
|
1787
|
+
threatIntelScore: 1.0,
|
|
1788
|
+
virtualizationScore: 0.8
|
|
1817
1789
|
};
|
|
1818
1790
|
|
|
1819
|
-
//
|
|
1791
|
+
// An "individual" is a complete security configuration object
|
|
1820
1792
|
const createIndividual = () => ({
|
|
1821
1793
|
thresholds: {
|
|
1822
1794
|
low: 15 + random() * 20, // 15-35
|
|
@@ -1847,10 +1819,10 @@ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
|
1847
1819
|
}
|
|
1848
1820
|
});
|
|
1849
1821
|
|
|
1850
|
-
//
|
|
1822
|
+
// Crossover and mutation operators for complex structured configurations
|
|
1851
1823
|
const crossover = (c1, c2) => {
|
|
1852
1824
|
const child = JSON.parse(JSON.stringify(c1)); // Deep copy
|
|
1853
|
-
//
|
|
1825
|
+
// Crossover across each parameter section
|
|
1854
1826
|
for (const key in child.thresholds) {
|
|
1855
1827
|
child.thresholds[key] = (c1.thresholds[key] + c2.thresholds[key]) / 2;
|
|
1856
1828
|
}
|
|
@@ -1866,9 +1838,8 @@ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
|
1866
1838
|
const mutate = (c, currentConfig) => {
|
|
1867
1839
|
const newConfig = JSON.parse(JSON.stringify(c));
|
|
1868
1840
|
|
|
1869
|
-
//
|
|
1870
|
-
//
|
|
1871
|
-
// car ils ont un impact plus direct sur la détection que les seuils.
|
|
1841
|
+
// Weighted section selection: prioritize 'patterns' and 'weights'
|
|
1842
|
+
// as they have a more direct impact on detection accuracy than thresholds.
|
|
1872
1843
|
const sections = [
|
|
1873
1844
|
{ name: 'patterns', weight: 0.50 },
|
|
1874
1845
|
{ name: 'thresholds', weight: 0.25 },
|
|
@@ -1889,7 +1860,7 @@ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
|
1889
1860
|
const keyToMutate = keys[crypto.randomInt(0, keys.length)];
|
|
1890
1861
|
|
|
1891
1862
|
|
|
1892
|
-
//
|
|
1863
|
+
// Ensure mutated parameters stay within reasonable bounds
|
|
1893
1864
|
if (sectionToMutate === 'weights') {
|
|
1894
1865
|
newConfig[sectionToMutate][keyToMutate] = Math.max(0.05, Math.min(1.5, newConfig[sectionToMutate][keyToMutate] + (random() - 0.5) * 0.1));
|
|
1895
1866
|
} else if (sectionToMutate === 'thresholds') {
|
|
@@ -1902,10 +1873,10 @@ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
|
1902
1873
|
}
|
|
1903
1874
|
}
|
|
1904
1875
|
|
|
1905
|
-
//
|
|
1876
|
+
// Maximum drift constraint (+/- 30% relative to current reference configuration)
|
|
1906
1877
|
if (currentConfig && currentConfig[sectionToMutate] && currentConfig[sectionToMutate][keyToMutate] !== undefined) {
|
|
1907
1878
|
const originalValue = currentConfig[sectionToMutate][keyToMutate];
|
|
1908
|
-
if (typeof originalValue === 'number' && originalValue !== 0) { //
|
|
1879
|
+
if (typeof originalValue === 'number' && originalValue !== 0) { // Avoid division by zero or locking zeroes
|
|
1909
1880
|
const minAllowed = originalValue * 0.7; // -30%
|
|
1910
1881
|
const maxAllowed = originalValue * 1.3; // +30%
|
|
1911
1882
|
newConfig[sectionToMutate][keyToMutate] = Math.max(minAllowed, Math.min(maxAllowed, newConfig[sectionToMutate][keyToMutate]));
|
|
@@ -1919,7 +1890,7 @@ Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
|
1919
1890
|
createIndividual,
|
|
1920
1891
|
fitnessFunction,
|
|
1921
1892
|
crossover,
|
|
1922
|
-
(c) => mutate(c, context.currentConfig), //
|
|
1893
|
+
(c) => mutate(c, context.currentConfig), // Pass currentConfig to mutation function
|
|
1923
1894
|
{ generations: 50, populationSize: 50, ...options }
|
|
1924
1895
|
);
|
|
1925
1896
|
};
|