blackopsrepl/portfolio-optimization-python
1
1/**
2 * Advanced Configuration Module
3 *
4 * This module provides advanced configuration options for the portfolio optimization
5 * quickstart, including presets and configurable solver parameters.
6 *
7 * NOTE: This is an optional enhancement. The core quickstart works without this module.
8 * To use it, include this script after app.js and call initAdvancedConfig().
9 */
10
11// =============================================================================
12// Configuration Presets
13// =============================================================================
14
15const PRESETS = {
16 conservative: {
17 targetStocks: 30,
18 maxSector: 15,
19 solverTime: 60,
20 description: "Max diversification: 30 stocks, tight sector limits, longer solve time."
21 },
22 balanced: {
23 targetStocks: 20,
24 maxSector: 25,
25 solverTime: 30,
26 description: "Balanced settings for typical portfolio optimization."
27 },
28 aggressive: {
29 targetStocks: 10,
30 maxSector: 40,
31 solverTime: 30,
32 description: "Concentrated bets: fewer stocks, looser sector limits."
33 },
34 quick: {
35 targetStocks: 20,
36 maxSector: 25,
37 solverTime: 10,
38 description: "Fast iteration: same as balanced but 10s solve time."
39 }
40};
41
42// Current configuration state
43let currentConfig = { ...PRESETS.balanced };
44
45// =============================================================================
46// Initialization
47// =============================================================================
48
49/**
50 * Initialize advanced configuration UI and event handlers.
51 * Call this after the DOM is ready.
52 */
53function initAdvancedConfig() {
54 // Preset selector
55 $("#presetSelector").change(function() {
56 const preset = $(this).val();
57 if (preset !== "custom" && PRESETS[preset]) {
58 // Use Object.assign to mutate in place - preserves window.currentConfig reference
59 Object.assign(currentConfig, PRESETS[preset]);
60 updateConfigSliders();
61 updatePresetDescription(preset);
62 applyConfigToLoadedPlan();
63 }
64 });
65
66 // Target stocks slider
67 $("#targetStocksSlider").on("input", function() {
68 currentConfig.targetStocks = parseInt(this.value);
69 $("#targetStocksValue").text(this.value);
70 markAsCustom();
71 applyConfigToLoadedPlan();
72 });
73
74 // Max sector slider
75 $("#maxSectorSlider").on("input", function() {
76 currentConfig.maxSector = parseInt(this.value);
77 $("#maxSectorValue").text(this.value + "%");
78 markAsCustom();
79 applyConfigToLoadedPlan();
80 });
81
82 // Solver time slider
83 $("#solverTimeSlider").on("input", function() {
84 currentConfig.solverTime = parseInt(this.value);
85 $("#solverTimeValue").text(formatSolverTime(this.value));
86 markAsCustom();
87 applyConfigToLoadedPlan();
88 });
89}
90
91// =============================================================================
92// Configuration UI Updates
93// =============================================================================
94
95function updateConfigSliders() {
96 $("#targetStocksSlider").val(currentConfig.targetStocks);
97 $("#targetStocksValue").text(currentConfig.targetStocks);
98
99 $("#maxSectorSlider").val(currentConfig.maxSector);
100 $("#maxSectorValue").text(currentConfig.maxSector + "%");
101
102 $("#solverTimeSlider").val(currentConfig.solverTime);
103 $("#solverTimeValue").text(formatSolverTime(currentConfig.solverTime));
104}
105
106function formatSolverTime(seconds) {
107 if (seconds >= 60) {
108 const mins = Math.floor(seconds / 60);
109 const secs = seconds % 60;
110 return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
111 }
112 return `${seconds}s`;
113}
114
115function markAsCustom() {
116 // Check if current settings match any preset
117 for (const [name, preset] of Object.entries(PRESETS)) {
118 if (preset.targetStocks === currentConfig.targetStocks &&
119 preset.maxSector === currentConfig.maxSector &&
120 preset.solverTime === currentConfig.solverTime) {
121 $("#presetSelector").val(name);
122 updatePresetDescription(name);
123 return;
124 }
125 }
126 // No match - mark as custom
127 $("#presetSelector").val("custom");
128 updatePresetDescription("custom");
129}
130
131function updatePresetDescription(preset) {
132 const descriptions = {
133 conservative: PRESETS.conservative.description,
134 balanced: PRESETS.balanced.description,
135 aggressive: PRESETS.aggressive.description,
136 quick: PRESETS.quick.description,
137 custom: "Custom configuration. Adjust sliders to your needs."
138 };
139 $("#presetDescription").text(descriptions[preset] || descriptions.custom);
140}
141
142// =============================================================================
143// Apply Configuration to Plan
144// =============================================================================
145
146/**
147 * Apply current configuration to the loaded plan.
148 * This updates the plan object that will be sent to the solver.
149 */
150function applyConfigToLoadedPlan() {
151 // loadedPlan is defined in app.js
152 if (typeof loadedPlan !== 'undefined' && loadedPlan) {
153 loadedPlan.targetPositionCount = currentConfig.targetStocks;
154 loadedPlan.maxSectorPercentage = currentConfig.maxSector / 100;
155 loadedPlan.solverConfig = {
156 terminationSeconds: currentConfig.solverTime
157 };
158 }
159}
160
161/**
162 * Get the current configuration.
163 * @returns {Object} Current configuration with targetStocks, maxSector, solverTime
164 */
165function getCurrentConfig() {
166 return { ...currentConfig };
167}
168
169/**
170 * Get target stock count from current configuration.
171 * Used by app.js for KPI display.
172 */
173function getTargetStockCount() {
174 return currentConfig.targetStocks;
175}
176
177// Export for use in app.js
178window.PRESETS = PRESETS;
179window.currentConfig = currentConfig;
180window.initAdvancedConfig = initAdvancedConfig;
181window.applyConfigToLoadedPlan = applyConfigToLoadedPlan;
182window.getCurrentConfig = getCurrentConfig;
183window.getTargetStockCount = getTargetStockCount;
184 