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