dexbot 1.6.4 → 1.6.5

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.
Files changed (126) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -12
  3. package/analysis/README.md +8 -1
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/analyze_dynamic_weight.ts +9 -3
  6. package/analysis/tradingview/analyze_tradingview.ts +5 -0
  7. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +2 -2
  8. package/analysis/trend_detection/DYNAMIC_WEIGHT_RESEARCH.md +35 -2
  9. package/analysis/trend_detection/dynamic_weight_chart_generator.ts +82 -10
  10. package/analysis/trend_detection/package.json +1 -1
  11. package/claw/modules/dexbot_profiles.ts +4 -3
  12. package/claw/package.json +1 -1
  13. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  14. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  15. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  16. package/claw/tests/test_dexbot_profiles.ts +18 -1
  17. package/dist/analysis/analyze_dynamic_weight.js +9 -3
  18. package/dist/analysis/analyze_dynamic_weight.js.map +1 -1
  19. package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
  20. package/dist/analysis/tradingview/analyze_tradingview.js +5 -0
  21. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  22. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +2 -2
  23. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -1
  24. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +82 -10
  25. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -1
  26. package/dist/dexbot.d.ts.map +1 -1
  27. package/dist/dexbot.js +27 -68
  28. package/dist/dexbot.js.map +1 -1
  29. package/dist/market_adapter/core/asymmetric_bounds.d.ts +2 -2
  30. package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
  31. package/dist/market_adapter/core/asymmetric_bounds.js +15 -12
  32. package/dist/market_adapter/core/asymmetric_bounds.js.map +1 -1
  33. package/dist/market_adapter/interval_utils.d.ts +1 -1
  34. package/dist/market_adapter/interval_utils.js +1 -1
  35. package/dist/modules/account_bots.d.ts +55 -5
  36. package/dist/modules/account_bots.d.ts.map +1 -1
  37. package/dist/modules/account_bots.js +258 -117
  38. package/dist/modules/account_bots.js.map +1 -1
  39. package/dist/modules/bot_defaults.d.ts +98 -0
  40. package/dist/modules/bot_defaults.d.ts.map +1 -0
  41. package/dist/modules/bot_defaults.js +169 -0
  42. package/dist/modules/bot_defaults.js.map +1 -0
  43. package/dist/modules/bot_settings.d.ts.map +1 -1
  44. package/dist/modules/bot_settings.js +4 -1
  45. package/dist/modules/bot_settings.js.map +1 -1
  46. package/dist/modules/config.d.ts +1 -0
  47. package/dist/modules/config.d.ts.map +1 -1
  48. package/dist/modules/config.js +1 -0
  49. package/dist/modules/config.js.map +1 -1
  50. package/dist/modules/constants.d.ts +11 -2
  51. package/dist/modules/constants.d.ts.map +1 -1
  52. package/dist/modules/constants.js +40 -27
  53. package/dist/modules/constants.js.map +1 -1
  54. package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -1
  55. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  56. package/dist/modules/dexbot_maintenance_runtime.js +9 -4
  57. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  58. package/dist/modules/launcher/adapter_requirement.js +1 -1
  59. package/dist/modules/launcher/launch_modes.d.ts +17 -1
  60. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  61. package/dist/modules/launcher/launch_modes.js +18 -1
  62. package/dist/modules/launcher/launch_modes.js.map +1 -1
  63. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  64. package/dist/modules/launcher/monolithic_runtime.js +8 -1
  65. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  66. package/dist/modules/market_adapter_whitelist.d.ts +40 -1
  67. package/dist/modules/market_adapter_whitelist.d.ts.map +1 -1
  68. package/dist/modules/market_adapter_whitelist.js +161 -11
  69. package/dist/modules/market_adapter_whitelist.js.map +1 -1
  70. package/dist/modules/order/grid.d.ts +9 -0
  71. package/dist/modules/order/grid.d.ts.map +1 -1
  72. package/dist/modules/order/grid.js +23 -5
  73. package/dist/modules/order/grid.js.map +1 -1
  74. package/dist/modules/order/manager.js +3 -2
  75. package/dist/modules/order/manager.js.map +1 -1
  76. package/dist/modules/order/utils/system.d.ts +5 -1
  77. package/dist/modules/order/utils/system.d.ts.map +1 -1
  78. package/dist/modules/order/utils/system.js +5 -2
  79. package/dist/modules/order/utils/system.js.map +1 -1
  80. package/dist/modules/settings_merge.d.ts +10 -1
  81. package/dist/modules/settings_merge.d.ts.map +1 -1
  82. package/dist/modules/settings_merge.js +27 -17
  83. package/dist/modules/settings_merge.js.map +1 -1
  84. package/dist/modules/validate_profiles.d.ts.map +1 -1
  85. package/dist/modules/validate_profiles.js +4 -3
  86. package/dist/modules/validate_profiles.js.map +1 -1
  87. package/dist/scripts/chart_command.d.ts +48 -0
  88. package/dist/scripts/chart_command.d.ts.map +1 -0
  89. package/dist/scripts/chart_command.js +504 -0
  90. package/dist/scripts/chart_command.js.map +1 -0
  91. package/dist/scripts/dw.d.ts +3 -0
  92. package/dist/scripts/dw.d.ts.map +1 -0
  93. package/dist/scripts/dw.js +30 -0
  94. package/dist/scripts/dw.js.map +1 -0
  95. package/dist/scripts/tv.d.ts +1 -31
  96. package/dist/scripts/tv.d.ts.map +1 -1
  97. package/dist/scripts/tv.js +10 -463
  98. package/dist/scripts/tv.js.map +1 -1
  99. package/dist/scripts/update.js +99 -62
  100. package/dist/scripts/update.js.map +1 -1
  101. package/dist/scripts/update_dist_freshness.d.ts +46 -0
  102. package/dist/scripts/update_dist_freshness.d.ts.map +1 -0
  103. package/dist/scripts/update_dist_freshness.js +115 -0
  104. package/dist/scripts/update_dist_freshness.js.map +1 -0
  105. package/dist/unlock.js +1 -1
  106. package/dist/unlock.js.map +1 -1
  107. package/docs/BITSHARES_ONBOARDING.md +10 -7
  108. package/docs/DEXBOT_COMPARISON.md +3 -3
  109. package/docs/EVOLUTION.md +8 -7
  110. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +4 -4
  111. package/docs/GRID_RECALCULATION.md +20 -24
  112. package/docs/GRID_RECONCILE.md +2 -2
  113. package/docs/LOGGING.md +1 -0
  114. package/docs/README.md +16 -16
  115. package/docs/WORKFLOW.md +1 -2
  116. package/docs/architecture.md +2 -2
  117. package/docs/developer_guide.md +1 -0
  118. package/market_adapter/README.md +36 -60
  119. package/modules/README.md +2 -1
  120. package/package.json +1 -2
  121. package/scripts/README.md +18 -25
  122. package/scripts/reset-settings.sh +1 -1
  123. package/dist/scripts/generate_market_adapter_whitelist.d.ts +0 -15
  124. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +0 -1
  125. package/dist/scripts/generate_market_adapter_whitelist.js +0 -191
  126. package/dist/scripts/generate_market_adapter_whitelist.js.map +0 -1
@@ -49,12 +49,13 @@
49
49
  * "active": true,
50
50
  * "dryRun": false,
51
51
  * "startPrice": "pool", // Price for order alignment: "pool", "book", or numeric
52
- * "gridPrice": null, // Reference price for x-factor bounds (3 options):
53
- * // "pool" / "book" = live pair price reference
52
+ * "gridPrice": "ama3", // Reference price for x-factor bounds (default "ama3"):
54
53
  * // "ama"/"ama1".."ama4" = market adapter writes a center snapshot to
55
54
  * // profiles/orders/<botKey>.dynamicgrid.json; grid reads the effective center on reset
56
- * // <number> = fixed numeric reference
57
- * // null = use startPrice
55
+ * // "pool" / "book" = live pair price reference (legacy, editor no longer offers it)
56
+ * // <number> = fixed numeric reference (legacy)
57
+ * // null = use startPrice (legacy); also accepted: false, empty/blank,
58
+ * // and the "none"/"null"/"start"/"startprice"/"s"/"n"/"no"/"f"/"0" spellings
58
59
  * "minPrice": "2x",
59
60
  * "maxPrice": "2x",
60
61
  * "incrementPercent": 0.5,
@@ -87,11 +88,13 @@ import { path } from './path_api.js';
87
88
  import { getStorage } from './storage/index.js';
88
89
  import { ensureProfilesDirectory, readInput, sleep } from './order/utils/system.js';
89
90
  import { setGlobalConsoleLevel, getGlobalConsoleLevel } from './order/logger.js';
90
- import { DEFAULT_CONFIG, GRID_LIMITS, TIMING, RANGE_QUALITY, LOG_LEVEL, UPDATER, MARKET_ADAPTER, NODE_MANAGEMENT, FILL_PROCESSING, PIPELINE_TIMING, CREDENTIAL_PROMPTS, MAINTENANCE, COW_PERFORMANCE, INCREMENT_BOUNDS, FEE_PARAMETERS, API_LIMITS, LOGGING_CONFIG, NATIVE_CLIENT, LAUNCHER } from './constants.js';
91
+ import { GRID_LIMITS, RANGE_QUALITY, MARKET_ADAPTER, NODE_MANAGEMENT, INCREMENT_BOUNDS, buildDefaultGeneralSettings } from './constants.js';
92
+ import { seedBotDraft, isUnsetGridPrice } from './bot_defaults.js';
91
93
  import { PATHS } from './paths.js';
92
94
  import { SETTINGS_FILE, readGeneralSettings, writeGeneralSettings } from './general_settings.js';
93
95
  import { parseJsonWithComments } from './order/utils/system.js';
94
- import { assertNoDuplicateBotKeys, loadSettingsFile } from './bot_settings.js';
96
+ import { assertNoDuplicateBotKeys, loadSettingsFile, normalizeBotEntry } from './bot_settings.js';
97
+ import { getWhitelistFlags, setWhitelistFlags, renameWhitelistEntry, removeWhitelistEntry, whitelistFile } from './market_adapter_whitelist.js';
95
98
  import { BOT_LIVE_CONFIG_KEYS } from './runtime_settings.js';
96
99
  import { mergeSettings } from './settings_merge.js';
97
100
  import { getErrorMessage } from './utils/errors.js';
@@ -157,26 +160,9 @@ function saveBotsConfig(config, filePath) {
157
160
  * @returns {Object} The loaded settings or default settings if the file doesn't exist.
158
161
  */
159
162
  function loadGeneralSettings() {
160
- const defaults = {
161
- LOG_LEVEL: LOG_LEVEL,
162
- GRID_LIMITS: { ...GRID_LIMITS, GRID_COMPARISON: { ...GRID_LIMITS.GRID_COMPARISON } },
163
- TIMING: { ...TIMING },
164
- UPDATER: { ...UPDATER },
165
- MARKET_ADAPTER: { ...MARKET_ADAPTER },
166
- NODE_MANAGEMENT: { ...NODE_MANAGEMENT },
167
- DEFAULT_CONFIG: { ...DEFAULT_CONFIG },
168
- FILL_PROCESSING: { ...FILL_PROCESSING },
169
- PIPELINE_TIMING: { ...PIPELINE_TIMING },
170
- CREDENTIAL_PROMPTS: { ...CREDENTIAL_PROMPTS },
171
- MAINTENANCE: { ...MAINTENANCE },
172
- COW_PERFORMANCE: { ...COW_PERFORMANCE },
173
- INCREMENT_BOUNDS: { ...INCREMENT_BOUNDS },
174
- FEE_PARAMETERS: { ...FEE_PARAMETERS },
175
- API_LIMITS: { ...API_LIMITS },
176
- LOGGING_CONFIG: { ...LOGGING_CONFIG },
177
- NATIVE_CLIENT: { ...NATIVE_CLIENT },
178
- LAUNCHER: { ...LAUNCHER },
179
- };
163
+ // Single canonical defaults document (modules/constants.ts) — same source
164
+ // the first-run generator and the local-overrides merge use.
165
+ const defaults = buildDefaultGeneralSettings();
180
166
  const settings = readGeneralSettings({
181
167
  fallback: null,
182
168
  onError: (err) => {
@@ -190,6 +176,14 @@ function loadGeneralSettings() {
190
176
  ? configuredDeltaPercent
191
177
  : MARKET_ADAPTER.AMA_DELTA_THRESHOLD_PERCENT;
192
178
  merged.MARKET_ADAPTER.AMA_DELTA_THRESHOLD_PERCENT = effectiveDeltaPercent;
179
+ // MARKET_ADAPTER validation for AMA_SLOPE_DELTA_THRESHOLD_PERCENT (the
180
+ // slope trigger factor: (value/100) x maxSlopePct). Same >0 guard so a
181
+ // hand-edited 0/negative/NaN can never disable the slope reset silently.
182
+ const configuredSlopeDeltaPercent = Number(merged.MARKET_ADAPTER.AMA_SLOPE_DELTA_THRESHOLD_PERCENT);
183
+ const effectiveSlopeDeltaPercent = Number.isFinite(configuredSlopeDeltaPercent) && configuredSlopeDeltaPercent > 0
184
+ ? configuredSlopeDeltaPercent
185
+ : MARKET_ADAPTER.AMA_SLOPE_DELTA_THRESHOLD_PERCENT;
186
+ merged.MARKET_ADAPTER.AMA_SLOPE_DELTA_THRESHOLD_PERCENT = effectiveSlopeDeltaPercent;
193
187
  return merged;
194
188
  }
195
189
  /**
@@ -559,6 +553,13 @@ function isDynamicPriceSource(value) {
559
553
  const lower = value.trim().toLowerCase();
560
554
  return lower === 'pool' || lower === 'book' || /^ama(?:[1-4])?$/.test(lower);
561
555
  }
556
+ /**
557
+ * AMA grid-price values: the four explicit presets ama1..ama4 plus bare
558
+ * "ama" (the pair's default preset). Bare "ama" is a valid green value but is
559
+ * deliberately not listed in the prompt label. These are the ONLY values that
560
+ * color green — pool/book/numeric/none stay accepted, just displayed red.
561
+ */
562
+ const AMA_GRID_PRICE_PATTERN = /^ama(?:[1-4])?$/;
562
563
  /**
563
564
  * Colors a start-price value for display: green when it is a dynamic source
564
565
  * ("pool"/"book"), red when it is a fixed numeric price (no live rescaling).
@@ -573,26 +574,53 @@ function colorStartPriceValue(value) {
573
574
  return `${COLORS.red}${text}${COLORS.reset}`;
574
575
  }
575
576
  /**
576
- * Colors a grid-price value for display: green for dynamic sources
577
- * ("pool"/"book"/AMA keywords), red for fixed numeric references. A null
578
- * gridPrice delegates to startPrice, so the "startPrice" label inherits the
579
- * start-price coloring.
577
+ * Colors a grid-price value for display: GREEN only for AMA values
578
+ * ("ama"/"ama1".."ama4"), RED for everything else — pool/book references,
579
+ * numeric values and any unset grid price (null, false, blank/whitespace, and
580
+ * the "none"/"s"/"no"/"f"/"start" aliases) which delegates to startPrice and is
581
+ * therefore labeled "startPrice", always in red: the delegation itself is the
582
+ * thing the editor wants replaced by an AMA preset.
580
583
  * @param {*} value - The grid price value to color.
581
- * @param {*} [startPrice] - The bot's current startPrice for null resolution.
582
584
  * @returns {string} ANSI-colored value string.
583
585
  */
584
- function colorGridPriceValue(value, startPrice) {
585
- if (value === null || value === undefined) {
586
- const label = isDynamicPriceSource(startPrice)
587
- ? `${COLORS.green}startPrice${COLORS.reset}`
588
- : `${COLORS.red}startPrice${COLORS.reset}`;
589
- return label;
586
+ function colorGridPriceValue(value) {
587
+ if (isUnsetGridPrice(value)) {
588
+ return `${COLORS.red}startPrice${COLORS.reset}`;
590
589
  }
591
590
  const text = String(value);
592
- if (isDynamicPriceSource(text))
591
+ if (AMA_GRID_PRICE_PATTERN.test(text.trim().toLowerCase()))
593
592
  return `${COLORS.green}${text}${COLORS.reset}`;
594
593
  return `${COLORS.red}${text}${COLORS.reset}`;
595
594
  }
595
+ /**
596
+ * True when startPrice === "pool" and no poolRef is pinned, so the runtime
597
+ * auto-selects the pair's default pool for order alignment. Deliberately NOT
598
+ * keyed on gridPrice === "pool": anchoring the grid reference on the live
599
+ * market price is discouraged and stays red in the GridPrice field, so the
600
+ * Pool label must not read as a green healthy default for that case.
601
+ * @param {*} data - The bot draft.
602
+ * @returns {boolean}
603
+ */
604
+ function startPriceUsesDefaultPool(data) {
605
+ return String(data?.startPrice ?? '').trim().toLowerCase() === 'pool';
606
+ }
607
+ /**
608
+ * Colors the pool label for the editor summary. An unset poolRef does NOT
609
+ * mean "no pool": when startPrice is "pool" the runtime auto-selects the
610
+ * pair's default pool, so it reads green `default`. Otherwise the pin is
611
+ * inert, shown as grey `none` so it does not compete with live values (a
612
+ * gridPrice of "pool"/"book" is discouraged and stays red in its own field).
613
+ * Display only — the stored poolRef is never changed here.
614
+ * @param {*} data - The bot draft.
615
+ * @returns {string} ANSI-colored pinned pool ID, `default` (green), or `none` (grey).
616
+ */
617
+ function formatPoolRefLabel(data) {
618
+ if (data?.poolRef)
619
+ return String(data.poolRef);
620
+ if (startPriceUsesDefaultPool(data))
621
+ return `${COLORS.green}default${COLORS.reset}`;
622
+ return `${COLORS.gray}none${COLORS.reset}`;
623
+ }
596
624
  /**
597
625
  * Colors a boolean flag value for display: green when the flag is in its
598
626
  * healthy state, red when it is not.
@@ -600,13 +628,15 @@ function colorGridPriceValue(value, startPrice) {
600
628
  * @param {boolean} greenWhenTrue - True when `true` is the healthy state
601
629
  * (e.g. Active), false when `false` is
602
630
  * healthy (e.g. DryRun off).
631
+ * @param {string} [offColor] - Color for the unhealthy value (default red);
632
+ * pass a warn color for flags that are optional.
603
633
  * @returns {string} ANSI-colored "true"/"false" string.
604
634
  */
605
- function colorBooleanFlag(value, greenWhenTrue) {
635
+ function colorBooleanFlag(value, greenWhenTrue, offColor = COLORS.red) {
606
636
  const isTrue = !!value;
607
637
  const healthy = greenWhenTrue ? isTrue : !isTrue;
608
638
  const text = String(isTrue);
609
- return healthy ? `${COLORS.green}${text}${COLORS.reset}` : `${COLORS.red}${text}${COLORS.reset}`;
639
+ return healthy ? `${COLORS.green}${text}${COLORS.reset}` : `${offColor}${text}${COLORS.reset}`;
610
640
  }
611
641
  /**
612
642
  * Converts a cron string to a readable format (days delta and time).
@@ -892,20 +922,46 @@ async function askNumberOrPercentage(promptText, defaultValue) {
892
922
  }
893
923
  return parsed;
894
924
  }
925
+ /**
926
+ * Parses a raw boolean answer.
927
+ * Accepted: y/yes/true/1/t → true, n/no/false/0/f → false (case-insensitive);
928
+ * empty input keeps the current/default value; anything else is rejected so
929
+ * the caller can re-prompt (the old `startsWith('y')` rule silently parsed
930
+ * "true" as false).
931
+ * @param {string} raw - Raw prompt input.
932
+ * @param {boolean} [defaultValue] - Value returned for empty input.
933
+ * @returns {{ ok: boolean, value?: boolean }} ok=false for unrecognized input.
934
+ */
935
+ function parseBooleanInput(raw, defaultValue) {
936
+ const value = String(raw ?? '').trim().toLowerCase();
937
+ if (!value)
938
+ return { ok: true, value: !!defaultValue };
939
+ if (value === 'y' || value === 'yes' || value === 'true' || value === '1' || value === 't')
940
+ return { ok: true, value: true };
941
+ if (value === 'n' || value === 'no' || value === 'false' || value === '0' || value === 'f')
942
+ return { ok: true, value: false };
943
+ return { ok: false };
944
+ }
895
945
  /**
896
946
  * Prompts the user for a boolean value (Y/n).
947
+ * Enter keeps the current/default value; unrecognized input re-prompts with
948
+ * a hint instead of silently coercing. See parseBooleanInput for the exact
949
+ * accepted spellings.
897
950
  * @param {string} promptText - The prompt text to display.
898
951
  * @param {boolean} [defaultValue] - The default value to use if input is empty.
899
952
  * @returns {Promise<boolean|string>} The boolean value or '\x1b' if ESC.
900
953
  */
901
954
  async function askBoolean(promptText, defaultValue) {
902
955
  const label = defaultValue ? 'Y/n' : 'y/N';
903
- const raw = (await readInput(`${promptText} (${label}): `)).trim().toLowerCase();
904
- if (raw === '\x1b')
905
- return '\x1b';
906
- if (!raw)
907
- return !!defaultValue;
908
- return raw.startsWith('y');
956
+ for (;;) {
957
+ const raw = (await readInput(`${promptText} (${label}): `)).trim().toLowerCase();
958
+ if (raw === '\x1b')
959
+ return '\x1b';
960
+ const parsed = parseBooleanInput(raw, !!defaultValue);
961
+ if (parsed.ok)
962
+ return parsed.value;
963
+ console.log('Please enter y/yes/true or n/no/false (Enter keeps the current value).');
964
+ }
909
965
  }
910
966
  /**
911
967
  * Prompts the user for the start price (numeric or "pool"/"book").
@@ -969,38 +1025,49 @@ async function askPoolRef(promptText, currentValue) {
969
1025
  }
970
1026
  }
971
1027
  /**
972
- * Prompts the user for the grid price mode (pool, book, ama, numeric, or startprice).
1028
+ * Prompts the user for the grid price mode.
1029
+ * The label lists ama1..ama4; bare "ama" is accepted as well (green) but not
1030
+ * listed. The live colorizer echoes AMA input green and every other value
1031
+ * (pool/book/number/none) red — red only flags "not an AMA value", it does not
1032
+ * reject: pool, book, numeric references, none/null (delegate to startPrice)
1033
+ * and bare "ama" all stay accepted, exactly as before. Only input that is not
1034
+ * a value at all is rejected, with a red error line.
973
1035
  * @param {string} promptText - The prompt text to display.
974
- * @param {string} [defaultValue] - The default value to use if input is empty.
975
- * @param {*} [startPrice] - The bot's current startPrice, used to color the
976
- * "startPrice" default label when gridPrice is null.
977
- * @returns {Promise<string>} The grid price mode or '\x1b' if ESC.
1036
+ * @param {string} [defaultValue] - The current value, offered as the Enter default.
1037
+ * @returns {Promise<any>} The normalized grid-price value, the unchanged
1038
+ * current value on Enter, or '\x1b' if ESC.
978
1039
  */
979
- async function askGridPriceMode(promptText, defaultValue, startPrice) {
1040
+ async function askGridPriceMode(promptText, defaultValue) {
980
1041
  while (true) {
981
- const coloredDefault = defaultValue === null || defaultValue === undefined
982
- ? colorGridPriceValue(null, startPrice)
983
- : colorGridPriceValue(defaultValue);
984
- const raw = (await readInput(`${promptText} [${coloredDefault}]: `, {
985
- colorize: (input) => colorGridPriceValue(input)
986
- })).trim();
1042
+ const coloredDefault = colorGridPriceValue(defaultValue ?? null);
1043
+ const raw = await readInput(`${promptText} [${coloredDefault}]: `, {
1044
+ colorize: (input) => colorGridPriceValue(input),
1045
+ // Keep whitespace so a deactivating space can be told apart from a
1046
+ // bare Enter that means "keep current".
1047
+ trimInput: false
1048
+ });
987
1049
  if (raw === '\x1b')
988
1050
  return '\x1b';
989
- if (!raw)
990
- return defaultValue === undefined ? null : defaultValue;
991
- const lower = raw.toLowerCase();
992
- if (lower === 'none' || lower === 'null' || lower === 'start' || lower === 'startprice')
1051
+ // A bare Enter keeps the current value (an already-unset value is
1052
+ // normalized to null so it is stored as delegation). Whitespace-only
1053
+ // input is NOT a bare Enter: the live echo already previews it as red
1054
+ // startPrice, so it must overwrite with null — otherwise the preview
1055
+ // would promise startPrice while the old value silently survived.
1056
+ if (raw === '')
1057
+ return isUnsetGridPrice(defaultValue) ? null : (defaultValue === undefined ? null : defaultValue);
1058
+ const lower = raw.trim().toLowerCase();
1059
+ if (isUnsetGridPrice(lower))
993
1060
  return null;
994
1061
  if (lower === 'pool')
995
1062
  return lower;
996
1063
  if (lower === 'book')
997
1064
  return 'book';
998
- if (/^ama(?:[1-4])?$/.test(lower))
1065
+ if (AMA_GRID_PRICE_PATTERN.test(lower))
999
1066
  return lower;
1000
1067
  const num = Number(raw);
1001
1068
  if (Number.isFinite(num) && num > 0)
1002
1069
  return num;
1003
- console.log('Please enter: pool, book, ama, ama1..ama4, a positive number, or none/startprice.');
1070
+ console.log(`${COLORS.red}Please enter: ama1, ama2, ama3 or ama4 — also allowed: pool, book, ama, a positive number, or none (s/start, n/no/false/0/f).${COLORS.reset}`);
1004
1071
  }
1005
1072
  }
1006
1073
  /**
@@ -1010,36 +1077,10 @@ async function askGridPriceMode(promptText, defaultValue, startPrice) {
1010
1077
  * @returns {Object} A normalized bot draft.
1011
1078
  */
1012
1079
  function normalizeBotDraft(base = {}) {
1013
- const data = JSON.parse(JSON.stringify(base));
1014
- if (!data.weightDistribution)
1015
- data.weightDistribution = { ...DEFAULT_CONFIG.weightDistribution };
1016
- if (!data.botFunds)
1017
- data.botFunds = { ...DEFAULT_CONFIG.botFunds };
1018
- if (!data.activeOrders)
1019
- data.activeOrders = { ...DEFAULT_CONFIG.activeOrders };
1020
- if (typeof data.reserveOrders === 'number')
1021
- data.reserveOrders = { buy: Math.max(0, Math.floor(data.reserveOrders)), sell: 0 };
1022
- if (data.reserveOrders === undefined || data.reserveOrders === null || typeof data.reserveOrders !== 'object' || Array.isArray(data.reserveOrders))
1023
- data.reserveOrders = { ...DEFAULT_CONFIG.reserveOrders };
1024
- if (data.active === undefined)
1025
- data.active = DEFAULT_CONFIG.active;
1026
- if (data.dryRun === undefined)
1027
- data.dryRun = DEFAULT_CONFIG.dryRun;
1028
- if (data.minPrice === undefined)
1029
- data.minPrice = DEFAULT_CONFIG.minPrice;
1030
- if (data.maxPrice === undefined)
1031
- data.maxPrice = DEFAULT_CONFIG.maxPrice;
1032
- if (data.incrementPercent === undefined)
1033
- data.incrementPercent = DEFAULT_CONFIG.incrementPercent;
1034
- if (data.targetSpreadPercent === undefined)
1035
- data.targetSpreadPercent = DEFAULT_CONFIG.targetSpreadPercent;
1036
- if (data.startPrice === undefined)
1037
- data.startPrice = data.startPrice || DEFAULT_CONFIG.startPrice || 'pool';
1038
- if (data.gridPrice === undefined)
1039
- data.gridPrice = null;
1040
- delete data.gridPriceOffsetPct;
1041
- delete data.gridPriceOffsetClampToBounds;
1042
- return data;
1080
+ // Seeding rules live in modules/bot_defaults.ts (single defaults source).
1081
+ // Return type stays `any`: the editor prompt-flow treats drafts as dynamic
1082
+ // bags and its inference depends on it.
1083
+ return seedBotDraft(base);
1043
1084
  }
1044
1085
  /**
1045
1086
  * Resolve data.preferredAccount to a chain account ID and stamp data.accountId
@@ -1162,10 +1203,30 @@ async function ensureBotAccountId(data, timeoutMs = 15000, quiet = false, force
1162
1203
  /**
1163
1204
  * Interactive menu to edit bot data.
1164
1205
  * @param {Object} [base={}] - The initial bot data to edit.
1165
- * @returns {Promise<Object|null>} The edited bot data or null if cancelled.
1206
+ * @param {number} [index=0] - Position the edited bot will occupy in the
1207
+ * bots array (used to derive the same botKey the runtime uses for unnamed
1208
+ * fallback entries).
1209
+ * @param {number} [baseIndex=index] - Position the base entry currently
1210
+ * occupies (differs from `index` for copies, whose source sits elsewhere).
1211
+ * @returns {Promise<{ data: any, commitAdapter: (() => void)|null }|null>}
1212
+ * The edited draft plus a commit hook for staged `6) Adapter` flags — the
1213
+ * caller MUST run `commitAdapter()` only after its bots.json save succeeded
1214
+ * (so a failed save never leaves the whitelist out of sync), or null when
1215
+ * cancelled.
1166
1216
  */
1167
- async function promptBotData(base = {}) {
1217
+ async function promptBotData(base = {}, index = 0, baseIndex = index) {
1168
1218
  const data = normalizeBotDraft(base);
1219
+ // Market-adapter flags (6) Adapter) live in
1220
+ // profiles/market_adapter_whitelist.json keyed by botKey — the same file
1221
+ // the market adapter reads. Stage edits here and expose them through the
1222
+ // commit hook so Cancel (or a failed bots.json save) discards them
1223
+ // together with the rest of the draft.
1224
+ const baseEntry = base && typeof base === 'object' && Object.keys(base).length > 0 ? base : null;
1225
+ const baseBotKey = baseEntry ? String(normalizeBotEntry(baseEntry, baseIndex).botKey || '') : '';
1226
+ let adapterStaged = null;
1227
+ const isAmaGridPriceDraft = () => /^ama(?:[1-4])?$/.test(String(data.gridPrice ?? '').trim().toLowerCase());
1228
+ const adapterFlags = () => adapterStaged
1229
+ || getWhitelistFlags(baseBotKey || String(normalizeBotEntry(data, index).botKey || ''));
1169
1230
  let finished = false;
1170
1231
  let cancelled = false;
1171
1232
  let showMenu = true;
@@ -1173,17 +1234,28 @@ async function promptBotData(base = {}) {
1173
1234
  if (showMenu) {
1174
1235
  console.log(`\n${COLORS.bold}--- Bot Editor: ` + (data.name || 'New Bot') + ` ---${COLORS.reset}`);
1175
1236
  console.log(`${COLORS.yellowBold}1) Pair:${COLORS.reset} ${COLORS.cyan}${data.assetA || '?'} / ${data.assetB || '?'}${COLORS.reset}`);
1176
- console.log(`${COLORS.yellowBold}2) Identity:${COLORS.reset} ${COLORS.orange}Name:${COLORS.reset} ${data.name || '?'} , ${COLORS.orange}Account:${COLORS.reset} ${data.preferredAccount || '?'} , ${COLORS.orange}Active:${COLORS.reset} ${colorBooleanFlag(data.active, true)}, ${COLORS.orange}DryRun:${COLORS.reset} ${colorBooleanFlag(data.dryRun, false)}`);
1177
- console.log(`${COLORS.yellowBold}3) Price:${COLORS.reset} ${COLORS.orange}Range:${COLORS.reset} [${colorPriceRangeValue(data.minPrice)} - ${colorPriceRangeValue(data.maxPrice)}], ${COLORS.orange}Start:${COLORS.reset} ${colorStartPriceValue(data.startPrice)}, ${COLORS.orange}Pool:${COLORS.reset} ${data.poolRef || 'none'}, ${COLORS.orange}GridPrice:${COLORS.reset} ${colorGridPriceValue(data.gridPrice, data.startPrice)}`);
1178
- console.log(`${COLORS.yellowBold}4) Grid:${COLORS.reset} ${COLORS.orange}Weights:${COLORS.reset} (S:${data.weightDistribution.sell}, B:${data.weightDistribution.buy}), ${COLORS.orange}Incr:${COLORS.reset} ${data.incrementPercent}%, ${COLORS.orange}Spread:${COLORS.reset} ${data.targetSpreadPercent}%`);
1237
+ console.log(`${COLORS.yellowBold}2) Identity:${COLORS.reset} ${COLORS.orange}Name:${COLORS.reset} ${data.name || '?'} | ${COLORS.orange}Account:${COLORS.reset} ${data.preferredAccount || '?'} | ${COLORS.orange}Active:${COLORS.reset} ${colorBooleanFlag(data.active, true)}, ${COLORS.orange}DryRun:${COLORS.reset} ${colorBooleanFlag(data.dryRun, false)}`);
1238
+ console.log(`${COLORS.yellowBold}3) Price:${COLORS.reset} ${COLORS.orange}Range:${COLORS.reset} [${colorPriceRangeValue(data.minPrice)} - ${colorPriceRangeValue(data.maxPrice)}] | ${COLORS.orange}Start:${COLORS.reset} ${colorStartPriceValue(data.startPrice)}, ${COLORS.orange}Pool:${COLORS.reset} ${formatPoolRefLabel(data)} | ${COLORS.orange}GridPrice:${COLORS.reset} ${colorGridPriceValue(data.gridPrice)}`);
1239
+ console.log(`${COLORS.yellowBold}4) Grid:${COLORS.reset} ${COLORS.orange}Weights:${COLORS.reset} (S:${data.weightDistribution.sell}, B:${data.weightDistribution.buy}) | ${COLORS.orange}Incr:${COLORS.reset} ${data.incrementPercent}%, ${COLORS.orange}Spread:${COLORS.reset} ${data.targetSpreadPercent}%`);
1179
1240
  console.log(`${COLORS.yellowBold}5) Funding:${COLORS.reset} ${COLORS.orange}Sell:${COLORS.reset} ${colorPercentageInput(data.botFunds.sell)}, ${COLORS.orange}Buy:${COLORS.reset} ${colorPercentageInput(data.botFunds.buy)} | ${COLORS.orange}Orders:${COLORS.reset} (S:${data.activeOrders.sell}, B:${data.activeOrders.buy}) | ${COLORS.orange}Reserve:${COLORS.reset} (S:${data.reserveOrders?.sell ?? 0}, B:${data.reserveOrders?.buy ?? 0})`);
1241
+ {
1242
+ const flags = adapterFlags();
1243
+ const inert = !isAmaGridPriceDraft() && (flags.ama || flags.dynamicWeight || flags.asymmetricBounds);
1244
+ const hint = inert ? ` ${COLORS.red}(needs gridPrice=ama)${COLORS.reset}` : '';
1245
+ // Adapter flags read as a health state, not a neutral toggle:
1246
+ // green = on (colorBooleanFlag's greenWhenTrue form, same
1247
+ // helper section 2 uses for Active). Price is the gate for the
1248
+ // other two, so off stays red; Weight/Range are optional
1249
+ // riders, so off is a bright-yellow warning rather than an error.
1250
+ console.log(`${COLORS.yellowBold}6) Adapter:${COLORS.reset} ${COLORS.orange}Price:${COLORS.reset} ${colorBooleanFlag(flags.ama, true)}, ${COLORS.orange}Weight:${COLORS.reset} ${colorBooleanFlag(flags.dynamicWeight, true, COLORS.yellowBold)}, ${COLORS.orange}Range:${COLORS.reset} ${colorBooleanFlag(flags.asymmetricBounds, true, COLORS.yellowBold)}${hint}`);
1251
+ }
1180
1252
  console.log('--------------------------------------------------');
1181
1253
  console.log(`${COLORS.greenBold}S) Save & Exit${COLORS.reset}`);
1182
1254
  console.log(`${COLORS.white}C) Cancel (Discard changes)${COLORS.reset}`);
1183
1255
  showMenu = false;
1184
1256
  }
1185
1257
  const choice = (await readInput('Select section to edit or action: ', {
1186
- validate: (input) => ['1', '2', '3', '4', '5', 's', 'c'].includes(input.toLowerCase())
1258
+ validate: (input) => ['1', '2', '3', '4', '5', '6', 's', 'c'].includes(input.toLowerCase())
1187
1259
  })).trim().toLowerCase();
1188
1260
  if (choice === '\x1b') {
1189
1261
  finished = true;
@@ -1260,7 +1332,7 @@ async function promptBotData(base = {}) {
1260
1332
  const poolR = await askPoolRef('poolRef (pinned pool ID for price source)', data.poolRef);
1261
1333
  if (poolR === '\x1b')
1262
1334
  break;
1263
- const gp = await askGridPriceMode('gridPrice (pool/book/ama/number/none)', data.gridPrice, data.startPrice);
1335
+ const gp = await askGridPriceMode('gridPrice (ama1/ama2/ama3/ama4)', data.gridPrice);
1264
1336
  if (gp === '\x1b')
1265
1337
  break;
1266
1338
  data.minPrice = minP;
@@ -1318,6 +1390,29 @@ async function promptBotData(base = {}) {
1318
1390
  data.reserveOrders = { buy: rBuy, sell: rSell };
1319
1391
  showMenu = true;
1320
1392
  break;
1393
+ case '6': {
1394
+ // Per-bot market-adapter flags (Price = AMA pricing, Weight =
1395
+ // dynamic weights, Range = asymmetric range scaling).
1396
+ const flags = adapterFlags();
1397
+ if (!isAmaGridPriceDraft()) {
1398
+ console.log(`${COLORS.gray}Note: these flags only take effect when gridPrice is ama/ama1..ama4 (set in 3) Price).${COLORS.reset}`);
1399
+ }
1400
+ const price = await askBoolean('AMA pricing (Price)', flags.ama);
1401
+ if (price === '\x1b')
1402
+ break;
1403
+ const weight = await askBoolean('Dynamic weights (Weight)', flags.dynamicWeight);
1404
+ if (weight === '\x1b')
1405
+ break;
1406
+ const range = await askBoolean('Range scaling (Range)', flags.asymmetricBounds);
1407
+ if (range === '\x1b')
1408
+ break;
1409
+ adapterStaged = { ama: price, dynamicWeight: weight, asymmetricBounds: range };
1410
+ if (!price && (weight || range)) {
1411
+ console.log(`${COLORS.yellow}Note: Weight/Range only take effect while Price (AMA) is enabled.${COLORS.reset}`);
1412
+ }
1413
+ showMenu = true;
1414
+ break;
1415
+ }
1321
1416
  case 's':
1322
1417
  // Final basic validation before saving
1323
1418
  if (!data.name || !data.assetA || !data.assetB || !data.preferredAccount) {
@@ -1350,11 +1445,40 @@ async function promptBotData(base = {}) {
1350
1445
  }
1351
1446
  if (cancelled)
1352
1447
  return null;
1448
+ // Staged 6) Adapter flags become a commit hook instead of an immediate
1449
+ // write: the caller runs it only after its bots.json save succeeded, so
1450
+ // the whitelist can never end up updated by a save that was rejected
1451
+ // (duplicate bot keys, write error). Writes are skipped when the staged
1452
+ // flags already match what is stored — an unchanged save never creates a
1453
+ // placeholder entry.
1454
+ const stagedFlags = adapterStaged;
1455
+ const commitAdapter = stagedFlags ? () => {
1456
+ const newKey = String(normalizeBotEntry(data, index).botKey || '');
1457
+ const needRename = !!(baseBotKey && newKey && baseBotKey !== newKey);
1458
+ const current = getWhitelistFlags(baseBotKey || newKey);
1459
+ const unchanged = stagedFlags.ama === current.ama
1460
+ && stagedFlags.dynamicWeight === current.dynamicWeight
1461
+ && stagedFlags.asymmetricBounds === current.asymmetricBounds;
1462
+ if (needRename && !renameWhitelistEntry(baseBotKey, newKey)) {
1463
+ console.log(`${COLORS.yellow}Adapter flags not saved — see the warning above.${COLORS.reset}`);
1464
+ return;
1465
+ }
1466
+ // Unchanged flags need no write — a successful rename above already
1467
+ // migrated the entry under the new key.
1468
+ if (unchanged)
1469
+ return;
1470
+ if (!setWhitelistFlags(newKey, stagedFlags)) {
1471
+ console.log(`${COLORS.yellow}Warning: adapter flags not saved — could not write ${whitelistFile()}.${COLORS.reset}`);
1472
+ }
1473
+ else {
1474
+ console.log(`${COLORS.green}Adapter flags saved for '${newKey}'. Price/Weight are picked up on the adapter's next cycle; Range scaling applies after 'dexbot reset ${data.name}'.${COLORS.reset}`);
1475
+ }
1476
+ } : null;
1353
1477
  // Return the final data structure, preserving ALL fields from the normalized
1354
1478
  // draft (deep-copy of base + defaults, minus stripped runtime-managed fields).
1355
1479
  // A fixed whitelist here would silently drop custom overrides (logging, timing,
1356
1480
  // feeParams, gridLimits, poolRef, etc.) when the caller replaces the bot entry.
1357
- return { ...data };
1481
+ return { data: { ...data }, commitAdapter };
1358
1482
  }
1359
1483
  /**
1360
1484
  * Interactive menu to edit general settings.
@@ -1365,7 +1489,7 @@ async function promptGeneralSettings() {
1365
1489
  let finished = false;
1366
1490
  while (!finished) {
1367
1491
  console.log(`${COLORS.bold}--- General Settings (Global) ---${COLORS.reset}`);
1368
- console.log(`${COLORS.yellowBold}1) Grid Health:${COLORS.reset} ${COLORS.orange}Ratio:${COLORS.reset} ${settings.GRID_LIMITS.GRID_REGENERATION_PERCENTAGE}%, ${COLORS.orange}RMS:${COLORS.reset} ${settings.GRID_LIMITS.GRID_COMPARISON.RMS_PERCENTAGE}%, ${COLORS.orange}AMA Delta:${COLORS.reset} ${settings.MARKET_ADAPTER.AMA_DELTA_THRESHOLD_PERCENT}%`);
1492
+ console.log(`${COLORS.yellowBold}1) Grid Health:${COLORS.reset} ${COLORS.orange}Funds:${COLORS.reset} ${settings.GRID_LIMITS.GRID_REGENERATION_PERCENTAGE}%, ${COLORS.orange}RMS:${COLORS.reset} ${settings.GRID_LIMITS.GRID_COMPARISON.RMS_PERCENTAGE}%, ${COLORS.orange}AMA Δ:${COLORS.reset} ${settings.MARKET_ADAPTER.AMA_DELTA_THRESHOLD_PERCENT}%, ${COLORS.orange}AMA-Slope Δ:${COLORS.reset} ${settings.MARKET_ADAPTER.AMA_SLOPE_DELTA_THRESHOLD_PERCENT}%`);
1369
1493
  console.log(`${COLORS.yellowBold}2) Order Recovery:${COLORS.reset} ${COLORS.orange}Dust Threshold:${COLORS.reset} ${settings.GRID_LIMITS.PARTIAL_DUST_THRESHOLD_PERCENTAGE}%`);
1370
1494
  const nodeCount = (settings.NODES.list || []).length;
1371
1495
  const hcIntervalMin = ((settings.NODES.healthCheck?.intervalMs || NODE_MANAGEMENT.HEALTH_CHECK_INTERVAL_MS) / 60000).toFixed(0);
@@ -1387,18 +1511,22 @@ async function promptGeneralSettings() {
1387
1511
  }
1388
1512
  switch (choice) {
1389
1513
  case '1':
1390
- const gRegen = await askNumberWithBounds('Grid Ratio Regeneration %', settings.GRID_LIMITS.GRID_REGENERATION_PERCENTAGE, 0.1, 50);
1514
+ const gRegen = await askNumberWithBounds('Grid Funds Regeneration %', settings.GRID_LIMITS.GRID_REGENERATION_PERCENTAGE, 0.1, 50);
1391
1515
  if (gRegen === '\x1b')
1392
1516
  break;
1393
1517
  const rms = await askNumberWithBounds('RMS Divergence Threshold %', settings.GRID_LIMITS.GRID_COMPARISON.RMS_PERCENTAGE, 1, 100);
1394
1518
  if (rms === '\x1b')
1395
1519
  break;
1396
- const amaDelta = await askNumberWithBounds('AMA Delta Threshold %', settings.MARKET_ADAPTER.AMA_DELTA_THRESHOLD_PERCENT, 0.1, 50.0);
1520
+ const amaDelta = await askNumberWithBounds('AMA Δ', settings.MARKET_ADAPTER.AMA_DELTA_THRESHOLD_PERCENT, 0.1, 50.0);
1397
1521
  if (amaDelta === '\x1b')
1398
1522
  break;
1523
+ const amaSlopeDelta = await askNumberWithBounds('AMA-Slope Δ', settings.MARKET_ADAPTER.AMA_SLOPE_DELTA_THRESHOLD_PERCENT, 0.1, 100.0);
1524
+ if (amaSlopeDelta === '\x1b')
1525
+ break;
1399
1526
  settings.GRID_LIMITS.GRID_REGENERATION_PERCENTAGE = gRegen;
1400
1527
  settings.GRID_LIMITS.GRID_COMPARISON.RMS_PERCENTAGE = rms;
1401
1528
  settings.MARKET_ADAPTER.AMA_DELTA_THRESHOLD_PERCENT = amaDelta;
1529
+ settings.MARKET_ADAPTER.AMA_SLOPE_DELTA_THRESHOLD_PERCENT = amaSlopeDelta;
1402
1530
  break;
1403
1531
  case '2':
1404
1532
  const dust = await askNumberWithBounds('Partial Dust Threshold %', settings.GRID_LIMITS.PARTIAL_DUST_THRESHOLD_PERCENTAGE, 0.1, 50);
@@ -1528,12 +1656,13 @@ async function main() {
1528
1656
  case '1': {
1529
1657
  while (true) {
1530
1658
  try {
1531
- const entry = await promptBotData();
1532
- if (!entry)
1659
+ const result = await promptBotData({}, config.bots.length);
1660
+ if (!result)
1533
1661
  break;
1534
- config.bots.push(entry);
1662
+ config.bots.push(result.data);
1535
1663
  saveBotsConfig(config, filePath);
1536
- console.log(`\nAdded bot '${entry.name}' to ${path.basename(filePath)}.`);
1664
+ result.commitAdapter?.();
1665
+ console.log(`\nAdded bot '${result.data.name}' to ${path.basename(filePath)}.`);
1537
1666
  }
1538
1667
  catch (err) {
1539
1668
  console.log(`\n❌ Invalid input: ${getErrorMessage(err)}\n`);
@@ -1548,10 +1677,12 @@ async function main() {
1548
1677
  if (idx === null || idx === '\x1b')
1549
1678
  break;
1550
1679
  try {
1551
- const entry = await promptBotData(config.bots[idx]);
1552
- if (entry) {
1553
- config.bots[idx] = entry;
1680
+ const result = await promptBotData(config.bots[idx], idx);
1681
+ if (result) {
1682
+ config.bots[idx] = result.data;
1554
1683
  saveBotsConfig(config, filePath);
1684
+ result.commitAdapter?.();
1685
+ const entry = result.data;
1555
1686
  console.log(`saved settings '${entry.name}' in ${path.basename(filePath)}.\n`);
1556
1687
  console.log(`Live pickup (~1min, no reload needed): ${BOT_LIVE_CONFIG_KEYS.join(' / ')}.`);
1557
1688
  console.log(`Grid geometry needs 'dexbot reset ${entry.name}' (or 'dexbot reload' for everything at once); market/account changes need 'dexbot reload'.\n`);
@@ -1573,8 +1704,17 @@ async function main() {
1573
1704
  if (confirm === '\x1b')
1574
1705
  break;
1575
1706
  if (confirm) {
1707
+ // Derive the whitelist key before splicing (createBotKey
1708
+ // uses the index only for unnamed fallback entries).
1709
+ const removedKey = String(normalizeBotEntry(config.bots[idx], idx).botKey || '');
1576
1710
  const removed = config.bots.splice(idx, 1)[0];
1577
1711
  saveBotsConfig(config, filePath);
1712
+ // Only after the bots.json save succeeded, drop the
1713
+ // matching whitelist entry so a deleted bot leaves no
1714
+ // stale flags behind (name reuse would inherit them).
1715
+ if (!removeWhitelistEntry(removedKey)) {
1716
+ console.log(`${COLORS.yellow}Warning: could not remove whitelist entry '${removedKey}' — check ${whitelistFile()}.${COLORS.reset}`);
1717
+ }
1578
1718
  console.log(`Removed bot '${removed.name || placeholderName}' from ${path.basename(filePath)}.\n`);
1579
1719
  }
1580
1720
  else {
@@ -1589,11 +1729,12 @@ async function main() {
1589
1729
  if (idx === null || idx === '\x1b')
1590
1730
  break;
1591
1731
  try {
1592
- const entry = await promptBotData(config.bots[idx]);
1593
- if (entry) {
1594
- config.bots.splice(idx + 1, 0, entry);
1732
+ const result = await promptBotData(config.bots[idx], idx + 1, idx);
1733
+ if (result) {
1734
+ config.bots.splice(idx + 1, 0, result.data);
1595
1735
  saveBotsConfig(config, filePath);
1596
- console.log(`Copied bot '${entry.name}' into ${path.basename(filePath)}.\n`);
1736
+ result.commitAdapter?.();
1737
+ console.log(`Copied bot '${result.data.name}' into ${path.basename(filePath)}.\n`);
1597
1738
  }
1598
1739
  }
1599
1740
  catch (err) {
@@ -1617,5 +1758,5 @@ async function main() {
1617
1758
  }
1618
1759
  console.log('Botmanager closed!');
1619
1760
  }
1620
- export { main, normalizeBotDraft, ensureBotAccountId, parseJsonWithComments };
1761
+ export { main, normalizeBotDraft, ensureBotAccountId, parseJsonWithComments, parseBooleanInput, colorGridPriceValue, formatPoolRefLabel, loadGeneralSettings, saveGeneralSettings };
1621
1762
  //# sourceMappingURL=account_bots.js.map