backtest-kit 2.0.3 → 2.0.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.
package/build/index.cjs CHANGED
@@ -783,10 +783,14 @@ class ClientExchange {
783
783
  // Apply distinct by timestamp to remove duplicates
784
784
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
785
785
  if (filteredData.length !== uniqueData.length) {
786
- this.params.logger.warn(`ClientExchange Removed ${filteredData.length - uniqueData.length} duplicate candles by timestamp`);
786
+ const msg = `ClientExchange Removed ${filteredData.length - uniqueData.length} duplicate candles by timestamp`;
787
+ this.params.logger.warn(msg);
788
+ console.warn(msg);
787
789
  }
788
790
  if (uniqueData.length < limit) {
789
- this.params.logger.warn(`ClientExchange Expected ${limit} candles, got ${uniqueData.length}`);
791
+ const msg = `ClientExchange Expected ${limit} candles, got ${uniqueData.length}`;
792
+ this.params.logger.warn(msg);
793
+ console.warn(msg);
790
794
  }
791
795
  await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
792
796
  return uniqueData;
@@ -9896,11 +9900,218 @@ class RiskSchemaService {
9896
9900
  }
9897
9901
  }
9898
9902
 
9903
+ /**
9904
+ * List of valid method names allowed in action handlers.
9905
+ * Any public methods not in this list will trigger validation errors.
9906
+ * Private methods (starting with _ or #) are ignored during validation.
9907
+ */
9908
+ const VALID_METHOD_NAMES = [
9909
+ "init",
9910
+ "signal",
9911
+ "signalLive",
9912
+ "signalBacktest",
9913
+ "breakevenAvailable",
9914
+ "partialProfitAvailable",
9915
+ "partialLossAvailable",
9916
+ "pingScheduled",
9917
+ "pingActive",
9918
+ "riskRejection",
9919
+ "dispose",
9920
+ ];
9921
+ /**
9922
+ * Calculates the Levenshtein distance between two strings.
9923
+ *
9924
+ * Levenshtein distance is the minimum number of single-character edits
9925
+ * (insertions, deletions, or substitutions) required to change one string into another.
9926
+ * Used to find typos and similar method names in validation.
9927
+ *
9928
+ * @param str1 - First string to compare
9929
+ * @param str2 - Second string to compare
9930
+ * @returns Number of edits needed to transform str1 into str2
9931
+ */
9932
+ const LEVENSHTEIN_DISTANCE = (str1, str2) => {
9933
+ const len1 = str1.length;
9934
+ const len2 = str2.length;
9935
+ // Create a 2D array for dynamic programming
9936
+ const matrix = Array.from({ length: len1 + 1 }, () => Array(len2 + 1).fill(0));
9937
+ // Initialize first column and row
9938
+ for (let i = 0; i <= len1; i++) {
9939
+ matrix[i][0] = i;
9940
+ }
9941
+ for (let j = 0; j <= len2; j++) {
9942
+ matrix[0][j] = j;
9943
+ }
9944
+ // Fill the matrix
9945
+ for (let i = 1; i <= len1; i++) {
9946
+ for (let j = 1; j <= len2; j++) {
9947
+ const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
9948
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
9949
+ matrix[i][j - 1] + 1, // insertion
9950
+ matrix[i - 1][j - 1] + cost // substitution
9951
+ );
9952
+ }
9953
+ }
9954
+ return matrix[len1][len2];
9955
+ };
9956
+ /**
9957
+ * Finds suggestions for a method name based on similarity scoring.
9958
+ *
9959
+ * Uses Levenshtein distance and partial string matching to find similar method names.
9960
+ * Returns suggestions sorted by similarity (most similar first).
9961
+ * Used to provide helpful "Did you mean?" suggestions in validation error messages.
9962
+ *
9963
+ * @param methodName - The invalid method name to find suggestions for
9964
+ * @param validNames - List of valid method names to search through
9965
+ * @param maxDistance - Maximum Levenshtein distance to consider (default: 3)
9966
+ * @returns Array of suggested method names sorted by similarity
9967
+ */
9968
+ const FIND_SUGGESTIONS = (methodName, validNames, maxDistance = 3) => {
9969
+ const lowerMethodName = methodName.toLowerCase();
9970
+ // Calculate similarity score for each valid name
9971
+ const suggestions = validNames
9972
+ .map((validName) => {
9973
+ const lowerValidName = validName.toLowerCase();
9974
+ const distance = LEVENSHTEIN_DISTANCE(lowerMethodName, lowerValidName);
9975
+ // Check for partial matches
9976
+ const hasPartialMatch = lowerValidName.includes(lowerMethodName) ||
9977
+ lowerMethodName.includes(lowerValidName);
9978
+ return {
9979
+ name: validName,
9980
+ distance,
9981
+ hasPartialMatch,
9982
+ };
9983
+ })
9984
+ .filter((item) => item.distance <= maxDistance || item.hasPartialMatch)
9985
+ .sort((a, b) => {
9986
+ // Prioritize partial matches
9987
+ if (a.hasPartialMatch && !b.hasPartialMatch)
9988
+ return -1;
9989
+ if (!a.hasPartialMatch && b.hasPartialMatch)
9990
+ return 1;
9991
+ // Then sort by distance
9992
+ return a.distance - b.distance;
9993
+ })
9994
+ .slice(0, 3) // Limit to top 3 suggestions
9995
+ .map((item) => item.name);
9996
+ return suggestions;
9997
+ };
9998
+ /**
9999
+ * Validates that all public methods in a class-based action handler are in the allowed list.
10000
+ *
10001
+ * Inspects the class prototype to find all method names and ensures they match
10002
+ * the VALID_METHOD_NAMES list. Private methods (starting with _ or #) are skipped.
10003
+ * Private fields with # are not visible via Object.getOwnPropertyNames() and don't
10004
+ * need validation as they're truly private and inaccessible.
10005
+ *
10006
+ * @param actionName - Name of the action being validated
10007
+ * @param handler - Class constructor for the action handler
10008
+ * @param self - ActionSchemaService instance for logging
10009
+ * @throws Error if any public method is not in VALID_METHOD_NAMES
10010
+ */
10011
+ const VALIDATE_CLASS_METHODS = (actionName, handler, self) => {
10012
+ // Get all method names from prototype (for classes)
10013
+ // Note: Private fields with # are not visible via Object.getOwnPropertyNames()
10014
+ // and don't need validation as they're truly private and inaccessible
10015
+ const prototypeProps = Object.getOwnPropertyNames(handler.prototype);
10016
+ for (const methodName of prototypeProps) {
10017
+ // Skip constructor and conventionally private methods (starting with _)
10018
+ if (methodName === "constructor" || methodName.startsWith("_")) {
10019
+ continue;
10020
+ }
10021
+ const descriptor = Object.getOwnPropertyDescriptor(handler.prototype, methodName);
10022
+ const isMethod = descriptor && typeof descriptor.value === "function";
10023
+ if (isMethod && !VALID_METHOD_NAMES.includes(methodName)) {
10024
+ const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
10025
+ const lines = [
10026
+ `ActionSchema ${actionName} contains invalid method "${methodName}". `,
10027
+ `Valid methods are: ${VALID_METHOD_NAMES.join(", ")}`,
10028
+ ];
10029
+ if (suggestions.length > 0) {
10030
+ lines.push("");
10031
+ lines.push(`Do you mean: ${suggestions.join(", ")}?`);
10032
+ lines.push("");
10033
+ }
10034
+ lines.push(`If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`);
10035
+ const msg = functoolsKit.str.newline(lines);
10036
+ self.loggerService.log(`actionValidationService exception thrown`, {
10037
+ msg,
10038
+ });
10039
+ throw new Error(msg);
10040
+ }
10041
+ }
10042
+ };
10043
+ /**
10044
+ * Validates that all public methods in an object-based action handler are in the allowed list.
10045
+ *
10046
+ * Inspects the object's own properties to find all method names and ensures they match
10047
+ * the VALID_METHOD_NAMES list. Private properties (starting with _) are skipped.
10048
+ *
10049
+ * @param actionName - Name of the action being validated
10050
+ * @param handler - Plain object implementing partial IPublicAction interface
10051
+ * @param self - ActionSchemaService instance for logging
10052
+ * @throws Error if any public method is not in VALID_METHOD_NAMES
10053
+ */
10054
+ const VALIDATE_OBJECT_METHODS = (actionName, handler, self) => {
10055
+ // For plain objects (Partial<IPublicAction>)
10056
+ const methodNames = Object.keys(handler);
10057
+ for (const methodName of methodNames) {
10058
+ // Skip private properties (starting with _)
10059
+ if (methodName.startsWith("_")) {
10060
+ continue;
10061
+ }
10062
+ if (typeof handler[methodName] === "function" &&
10063
+ !VALID_METHOD_NAMES.includes(methodName)) {
10064
+ const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
10065
+ const lines = [
10066
+ `ActionSchema ${actionName} contains invalid method "${methodName}". `,
10067
+ `Valid methods are: ${VALID_METHOD_NAMES.join(", ")}`,
10068
+ ];
10069
+ if (suggestions.length > 0) {
10070
+ lines.push("");
10071
+ lines.push(`Do you mean: ${suggestions.join(", ")}?`);
10072
+ lines.push("");
10073
+ }
10074
+ lines.push(`If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`);
10075
+ const msg = functoolsKit.str.newline(lines);
10076
+ self.loggerService.log(`actionValidationService exception thrown`, {
10077
+ msg,
10078
+ });
10079
+ throw new Error(msg);
10080
+ }
10081
+ }
10082
+ };
9899
10083
  /**
9900
10084
  * Service for managing action schema registry.
9901
10085
  *
10086
+ * Manages registration, validation and retrieval of action schemas.
9902
10087
  * Uses ToolRegistry from functools-kit for type-safe schema storage.
9903
- * Action handlers are registered via addAction() and retrieved by name.
10088
+ * Validates that action handlers only contain allowed public methods
10089
+ * from the IPublicAction interface.
10090
+ *
10091
+ * Key features:
10092
+ * - Type-safe action schema registration
10093
+ * - Method name validation for class and object handlers
10094
+ * - Private method support (methods starting with _ or #)
10095
+ * - Schema override capabilities
10096
+ *
10097
+ * @example
10098
+ * ```typescript
10099
+ * // Register a class-based action
10100
+ * actionSchemaService.register("telegram-notifier", {
10101
+ * actionName: "telegram-notifier",
10102
+ * handler: TelegramNotifierAction,
10103
+ * callbacks: { ... }
10104
+ * });
10105
+ *
10106
+ * // Register an object-based action
10107
+ * actionSchemaService.register("logger", {
10108
+ * actionName: "logger",
10109
+ * handler: {
10110
+ * signal: async (event) => { ... },
10111
+ * dispose: async () => { ... }
10112
+ * }
10113
+ * });
10114
+ * ```
9904
10115
  */
9905
10116
  class ActionSchemaService {
9906
10117
  constructor() {
@@ -9909,9 +10120,13 @@ class ActionSchemaService {
9909
10120
  /**
9910
10121
  * Registers a new action schema.
9911
10122
  *
9912
- * @param key - Unique action name
9913
- * @param value - Action schema configuration
9914
- * @throws Error if action name already exists
10123
+ * Validates the schema structure and method names before registration.
10124
+ * Throws an error if the action name already exists in the registry.
10125
+ *
10126
+ * @param key - Unique action name identifier
10127
+ * @param value - Action schema configuration with handler and optional callbacks
10128
+ * @throws Error if action name already exists in registry
10129
+ * @throws Error if validation fails (missing required fields, invalid handler, invalid method names)
9915
10130
  */
9916
10131
  this.register = (key, value) => {
9917
10132
  this.loggerService.log(`actionSchemaService register`, { key });
@@ -9923,11 +10138,13 @@ class ActionSchemaService {
9923
10138
  *
9924
10139
  * Performs shallow validation to ensure all required properties exist
9925
10140
  * and have correct types before registration in the registry.
10141
+ * Also validates that all public methods in the handler are allowed.
9926
10142
  *
9927
10143
  * @param actionSchema - Action schema to validate
9928
10144
  * @throws Error if actionName is missing or not a string
9929
- * @throws Error if handler is missing or not a function
9930
- * @throws Error if callbacks is not an object
10145
+ * @throws Error if handler is not a function or plain object
10146
+ * @throws Error if handler contains invalid public method names
10147
+ * @throws Error if callbacks is provided but not an object
9931
10148
  */
9932
10149
  this.validateShallow = (actionSchema) => {
9933
10150
  this.loggerService.log(`actionSchemaService validateShallow`, {
@@ -9936,21 +10153,30 @@ class ActionSchemaService {
9936
10153
  if (typeof actionSchema.actionName !== "string") {
9937
10154
  throw new Error(`action schema validation failed: missing actionName`);
9938
10155
  }
9939
- if (typeof actionSchema.handler !== "function" && !functoolsKit.isObject(actionSchema.handler)) {
10156
+ if (typeof actionSchema.handler !== "function" &&
10157
+ !functoolsKit.isObject(actionSchema.handler)) {
9940
10158
  throw new Error(`action schema validation failed: handler is not a function or plain object for actionName=${actionSchema.actionName}`);
9941
10159
  }
9942
- if (actionSchema.callbacks &&
9943
- !functoolsKit.isObject(actionSchema.callbacks)) {
10160
+ if (typeof actionSchema.handler === "function" && actionSchema.handler.prototype) {
10161
+ VALIDATE_CLASS_METHODS(actionSchema.actionName, actionSchema.handler, this);
10162
+ }
10163
+ if (typeof actionSchema.handler === "object" && actionSchema.handler !== null) {
10164
+ VALIDATE_OBJECT_METHODS(actionSchema.actionName, actionSchema.handler, this);
10165
+ }
10166
+ if (actionSchema.callbacks && !functoolsKit.isObject(actionSchema.callbacks)) {
9944
10167
  throw new Error(`action schema validation failed: callbacks is not an object for actionName=${actionSchema.actionName}`);
9945
10168
  }
9946
10169
  };
9947
10170
  /**
9948
10171
  * Overrides an existing action schema with partial updates.
9949
10172
  *
10173
+ * Merges provided partial schema updates with the existing schema.
10174
+ * Useful for modifying handler or callbacks without re-registering the entire schema.
10175
+ *
9950
10176
  * @param key - Action name to override
9951
- * @param value - Partial schema updates
9952
- * @returns Updated action schema
9953
- * @throws Error if action name doesn't exist
10177
+ * @param value - Partial schema updates to merge
10178
+ * @returns Updated action schema after override
10179
+ * @throws Error if action name doesn't exist in registry
9954
10180
  */
9955
10181
  this.override = (key, value) => {
9956
10182
  this.loggerService.log(`actionSchemaService override`, { key });
@@ -9960,9 +10186,12 @@ class ActionSchemaService {
9960
10186
  /**
9961
10187
  * Retrieves an action schema by name.
9962
10188
  *
9963
- * @param key - Action name
10189
+ * Returns the complete action schema configuration including handler and callbacks.
10190
+ * Used internally by ActionConnectionService to instantiate ClientAction instances.
10191
+ *
10192
+ * @param key - Action name identifier
9964
10193
  * @returns Action schema configuration
9965
- * @throws Error if action name doesn't exist
10194
+ * @throws Error if action name doesn't exist in registry
9966
10195
  */
9967
10196
  this.get = (key) => {
9968
10197
  this.loggerService.log(`actionSchemaService get`, { key });
package/build/index.mjs CHANGED
@@ -763,10 +763,14 @@ class ClientExchange {
763
763
  // Apply distinct by timestamp to remove duplicates
764
764
  const uniqueData = Array.from(new Map(filteredData.map((candle) => [candle.timestamp, candle])).values());
765
765
  if (filteredData.length !== uniqueData.length) {
766
- this.params.logger.warn(`ClientExchange Removed ${filteredData.length - uniqueData.length} duplicate candles by timestamp`);
766
+ const msg = `ClientExchange Removed ${filteredData.length - uniqueData.length} duplicate candles by timestamp`;
767
+ this.params.logger.warn(msg);
768
+ console.warn(msg);
767
769
  }
768
770
  if (uniqueData.length < limit) {
769
- this.params.logger.warn(`ClientExchange Expected ${limit} candles, got ${uniqueData.length}`);
771
+ const msg = `ClientExchange Expected ${limit} candles, got ${uniqueData.length}`;
772
+ this.params.logger.warn(msg);
773
+ console.warn(msg);
770
774
  }
771
775
  await CALL_CANDLE_DATA_CALLBACKS_FN(this, symbol, interval, since, limit, uniqueData);
772
776
  return uniqueData;
@@ -9876,11 +9880,218 @@ class RiskSchemaService {
9876
9880
  }
9877
9881
  }
9878
9882
 
9883
+ /**
9884
+ * List of valid method names allowed in action handlers.
9885
+ * Any public methods not in this list will trigger validation errors.
9886
+ * Private methods (starting with _ or #) are ignored during validation.
9887
+ */
9888
+ const VALID_METHOD_NAMES = [
9889
+ "init",
9890
+ "signal",
9891
+ "signalLive",
9892
+ "signalBacktest",
9893
+ "breakevenAvailable",
9894
+ "partialProfitAvailable",
9895
+ "partialLossAvailable",
9896
+ "pingScheduled",
9897
+ "pingActive",
9898
+ "riskRejection",
9899
+ "dispose",
9900
+ ];
9901
+ /**
9902
+ * Calculates the Levenshtein distance between two strings.
9903
+ *
9904
+ * Levenshtein distance is the minimum number of single-character edits
9905
+ * (insertions, deletions, or substitutions) required to change one string into another.
9906
+ * Used to find typos and similar method names in validation.
9907
+ *
9908
+ * @param str1 - First string to compare
9909
+ * @param str2 - Second string to compare
9910
+ * @returns Number of edits needed to transform str1 into str2
9911
+ */
9912
+ const LEVENSHTEIN_DISTANCE = (str1, str2) => {
9913
+ const len1 = str1.length;
9914
+ const len2 = str2.length;
9915
+ // Create a 2D array for dynamic programming
9916
+ const matrix = Array.from({ length: len1 + 1 }, () => Array(len2 + 1).fill(0));
9917
+ // Initialize first column and row
9918
+ for (let i = 0; i <= len1; i++) {
9919
+ matrix[i][0] = i;
9920
+ }
9921
+ for (let j = 0; j <= len2; j++) {
9922
+ matrix[0][j] = j;
9923
+ }
9924
+ // Fill the matrix
9925
+ for (let i = 1; i <= len1; i++) {
9926
+ for (let j = 1; j <= len2; j++) {
9927
+ const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
9928
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
9929
+ matrix[i][j - 1] + 1, // insertion
9930
+ matrix[i - 1][j - 1] + cost // substitution
9931
+ );
9932
+ }
9933
+ }
9934
+ return matrix[len1][len2];
9935
+ };
9936
+ /**
9937
+ * Finds suggestions for a method name based on similarity scoring.
9938
+ *
9939
+ * Uses Levenshtein distance and partial string matching to find similar method names.
9940
+ * Returns suggestions sorted by similarity (most similar first).
9941
+ * Used to provide helpful "Did you mean?" suggestions in validation error messages.
9942
+ *
9943
+ * @param methodName - The invalid method name to find suggestions for
9944
+ * @param validNames - List of valid method names to search through
9945
+ * @param maxDistance - Maximum Levenshtein distance to consider (default: 3)
9946
+ * @returns Array of suggested method names sorted by similarity
9947
+ */
9948
+ const FIND_SUGGESTIONS = (methodName, validNames, maxDistance = 3) => {
9949
+ const lowerMethodName = methodName.toLowerCase();
9950
+ // Calculate similarity score for each valid name
9951
+ const suggestions = validNames
9952
+ .map((validName) => {
9953
+ const lowerValidName = validName.toLowerCase();
9954
+ const distance = LEVENSHTEIN_DISTANCE(lowerMethodName, lowerValidName);
9955
+ // Check for partial matches
9956
+ const hasPartialMatch = lowerValidName.includes(lowerMethodName) ||
9957
+ lowerMethodName.includes(lowerValidName);
9958
+ return {
9959
+ name: validName,
9960
+ distance,
9961
+ hasPartialMatch,
9962
+ };
9963
+ })
9964
+ .filter((item) => item.distance <= maxDistance || item.hasPartialMatch)
9965
+ .sort((a, b) => {
9966
+ // Prioritize partial matches
9967
+ if (a.hasPartialMatch && !b.hasPartialMatch)
9968
+ return -1;
9969
+ if (!a.hasPartialMatch && b.hasPartialMatch)
9970
+ return 1;
9971
+ // Then sort by distance
9972
+ return a.distance - b.distance;
9973
+ })
9974
+ .slice(0, 3) // Limit to top 3 suggestions
9975
+ .map((item) => item.name);
9976
+ return suggestions;
9977
+ };
9978
+ /**
9979
+ * Validates that all public methods in a class-based action handler are in the allowed list.
9980
+ *
9981
+ * Inspects the class prototype to find all method names and ensures they match
9982
+ * the VALID_METHOD_NAMES list. Private methods (starting with _ or #) are skipped.
9983
+ * Private fields with # are not visible via Object.getOwnPropertyNames() and don't
9984
+ * need validation as they're truly private and inaccessible.
9985
+ *
9986
+ * @param actionName - Name of the action being validated
9987
+ * @param handler - Class constructor for the action handler
9988
+ * @param self - ActionSchemaService instance for logging
9989
+ * @throws Error if any public method is not in VALID_METHOD_NAMES
9990
+ */
9991
+ const VALIDATE_CLASS_METHODS = (actionName, handler, self) => {
9992
+ // Get all method names from prototype (for classes)
9993
+ // Note: Private fields with # are not visible via Object.getOwnPropertyNames()
9994
+ // and don't need validation as they're truly private and inaccessible
9995
+ const prototypeProps = Object.getOwnPropertyNames(handler.prototype);
9996
+ for (const methodName of prototypeProps) {
9997
+ // Skip constructor and conventionally private methods (starting with _)
9998
+ if (methodName === "constructor" || methodName.startsWith("_")) {
9999
+ continue;
10000
+ }
10001
+ const descriptor = Object.getOwnPropertyDescriptor(handler.prototype, methodName);
10002
+ const isMethod = descriptor && typeof descriptor.value === "function";
10003
+ if (isMethod && !VALID_METHOD_NAMES.includes(methodName)) {
10004
+ const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
10005
+ const lines = [
10006
+ `ActionSchema ${actionName} contains invalid method "${methodName}". `,
10007
+ `Valid methods are: ${VALID_METHOD_NAMES.join(", ")}`,
10008
+ ];
10009
+ if (suggestions.length > 0) {
10010
+ lines.push("");
10011
+ lines.push(`Do you mean: ${suggestions.join(", ")}?`);
10012
+ lines.push("");
10013
+ }
10014
+ lines.push(`If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`);
10015
+ const msg = str.newline(lines);
10016
+ self.loggerService.log(`actionValidationService exception thrown`, {
10017
+ msg,
10018
+ });
10019
+ throw new Error(msg);
10020
+ }
10021
+ }
10022
+ };
10023
+ /**
10024
+ * Validates that all public methods in an object-based action handler are in the allowed list.
10025
+ *
10026
+ * Inspects the object's own properties to find all method names and ensures they match
10027
+ * the VALID_METHOD_NAMES list. Private properties (starting with _) are skipped.
10028
+ *
10029
+ * @param actionName - Name of the action being validated
10030
+ * @param handler - Plain object implementing partial IPublicAction interface
10031
+ * @param self - ActionSchemaService instance for logging
10032
+ * @throws Error if any public method is not in VALID_METHOD_NAMES
10033
+ */
10034
+ const VALIDATE_OBJECT_METHODS = (actionName, handler, self) => {
10035
+ // For plain objects (Partial<IPublicAction>)
10036
+ const methodNames = Object.keys(handler);
10037
+ for (const methodName of methodNames) {
10038
+ // Skip private properties (starting with _)
10039
+ if (methodName.startsWith("_")) {
10040
+ continue;
10041
+ }
10042
+ if (typeof handler[methodName] === "function" &&
10043
+ !VALID_METHOD_NAMES.includes(methodName)) {
10044
+ const suggestions = FIND_SUGGESTIONS(methodName, VALID_METHOD_NAMES);
10045
+ const lines = [
10046
+ `ActionSchema ${actionName} contains invalid method "${methodName}". `,
10047
+ `Valid methods are: ${VALID_METHOD_NAMES.join(", ")}`,
10048
+ ];
10049
+ if (suggestions.length > 0) {
10050
+ lines.push("");
10051
+ lines.push(`Do you mean: ${suggestions.join(", ")}?`);
10052
+ lines.push("");
10053
+ }
10054
+ lines.push(`If you want to keep this property name use one of these patterns: _${methodName} or #${methodName}`);
10055
+ const msg = str.newline(lines);
10056
+ self.loggerService.log(`actionValidationService exception thrown`, {
10057
+ msg,
10058
+ });
10059
+ throw new Error(msg);
10060
+ }
10061
+ }
10062
+ };
9879
10063
  /**
9880
10064
  * Service for managing action schema registry.
9881
10065
  *
10066
+ * Manages registration, validation and retrieval of action schemas.
9882
10067
  * Uses ToolRegistry from functools-kit for type-safe schema storage.
9883
- * Action handlers are registered via addAction() and retrieved by name.
10068
+ * Validates that action handlers only contain allowed public methods
10069
+ * from the IPublicAction interface.
10070
+ *
10071
+ * Key features:
10072
+ * - Type-safe action schema registration
10073
+ * - Method name validation for class and object handlers
10074
+ * - Private method support (methods starting with _ or #)
10075
+ * - Schema override capabilities
10076
+ *
10077
+ * @example
10078
+ * ```typescript
10079
+ * // Register a class-based action
10080
+ * actionSchemaService.register("telegram-notifier", {
10081
+ * actionName: "telegram-notifier",
10082
+ * handler: TelegramNotifierAction,
10083
+ * callbacks: { ... }
10084
+ * });
10085
+ *
10086
+ * // Register an object-based action
10087
+ * actionSchemaService.register("logger", {
10088
+ * actionName: "logger",
10089
+ * handler: {
10090
+ * signal: async (event) => { ... },
10091
+ * dispose: async () => { ... }
10092
+ * }
10093
+ * });
10094
+ * ```
9884
10095
  */
9885
10096
  class ActionSchemaService {
9886
10097
  constructor() {
@@ -9889,9 +10100,13 @@ class ActionSchemaService {
9889
10100
  /**
9890
10101
  * Registers a new action schema.
9891
10102
  *
9892
- * @param key - Unique action name
9893
- * @param value - Action schema configuration
9894
- * @throws Error if action name already exists
10103
+ * Validates the schema structure and method names before registration.
10104
+ * Throws an error if the action name already exists in the registry.
10105
+ *
10106
+ * @param key - Unique action name identifier
10107
+ * @param value - Action schema configuration with handler and optional callbacks
10108
+ * @throws Error if action name already exists in registry
10109
+ * @throws Error if validation fails (missing required fields, invalid handler, invalid method names)
9895
10110
  */
9896
10111
  this.register = (key, value) => {
9897
10112
  this.loggerService.log(`actionSchemaService register`, { key });
@@ -9903,11 +10118,13 @@ class ActionSchemaService {
9903
10118
  *
9904
10119
  * Performs shallow validation to ensure all required properties exist
9905
10120
  * and have correct types before registration in the registry.
10121
+ * Also validates that all public methods in the handler are allowed.
9906
10122
  *
9907
10123
  * @param actionSchema - Action schema to validate
9908
10124
  * @throws Error if actionName is missing or not a string
9909
- * @throws Error if handler is missing or not a function
9910
- * @throws Error if callbacks is not an object
10125
+ * @throws Error if handler is not a function or plain object
10126
+ * @throws Error if handler contains invalid public method names
10127
+ * @throws Error if callbacks is provided but not an object
9911
10128
  */
9912
10129
  this.validateShallow = (actionSchema) => {
9913
10130
  this.loggerService.log(`actionSchemaService validateShallow`, {
@@ -9916,21 +10133,30 @@ class ActionSchemaService {
9916
10133
  if (typeof actionSchema.actionName !== "string") {
9917
10134
  throw new Error(`action schema validation failed: missing actionName`);
9918
10135
  }
9919
- if (typeof actionSchema.handler !== "function" && !isObject(actionSchema.handler)) {
10136
+ if (typeof actionSchema.handler !== "function" &&
10137
+ !isObject(actionSchema.handler)) {
9920
10138
  throw new Error(`action schema validation failed: handler is not a function or plain object for actionName=${actionSchema.actionName}`);
9921
10139
  }
9922
- if (actionSchema.callbacks &&
9923
- !isObject(actionSchema.callbacks)) {
10140
+ if (typeof actionSchema.handler === "function" && actionSchema.handler.prototype) {
10141
+ VALIDATE_CLASS_METHODS(actionSchema.actionName, actionSchema.handler, this);
10142
+ }
10143
+ if (typeof actionSchema.handler === "object" && actionSchema.handler !== null) {
10144
+ VALIDATE_OBJECT_METHODS(actionSchema.actionName, actionSchema.handler, this);
10145
+ }
10146
+ if (actionSchema.callbacks && !isObject(actionSchema.callbacks)) {
9924
10147
  throw new Error(`action schema validation failed: callbacks is not an object for actionName=${actionSchema.actionName}`);
9925
10148
  }
9926
10149
  };
9927
10150
  /**
9928
10151
  * Overrides an existing action schema with partial updates.
9929
10152
  *
10153
+ * Merges provided partial schema updates with the existing schema.
10154
+ * Useful for modifying handler or callbacks without re-registering the entire schema.
10155
+ *
9930
10156
  * @param key - Action name to override
9931
- * @param value - Partial schema updates
9932
- * @returns Updated action schema
9933
- * @throws Error if action name doesn't exist
10157
+ * @param value - Partial schema updates to merge
10158
+ * @returns Updated action schema after override
10159
+ * @throws Error if action name doesn't exist in registry
9934
10160
  */
9935
10161
  this.override = (key, value) => {
9936
10162
  this.loggerService.log(`actionSchemaService override`, { key });
@@ -9940,9 +10166,12 @@ class ActionSchemaService {
9940
10166
  /**
9941
10167
  * Retrieves an action schema by name.
9942
10168
  *
9943
- * @param key - Action name
10169
+ * Returns the complete action schema configuration including handler and callbacks.
10170
+ * Used internally by ActionConnectionService to instantiate ClientAction instances.
10171
+ *
10172
+ * @param key - Action name identifier
9944
10173
  * @returns Action schema configuration
9945
- * @throws Error if action name doesn't exist
10174
+ * @throws Error if action name doesn't exist in registry
9946
10175
  */
9947
10176
  this.get = (key) => {
9948
10177
  this.loggerService.log(`actionSchemaService get`, { key });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -16569,8 +16569,35 @@ declare class RiskSchemaService {
16569
16569
  /**
16570
16570
  * Service for managing action schema registry.
16571
16571
  *
16572
+ * Manages registration, validation and retrieval of action schemas.
16572
16573
  * Uses ToolRegistry from functools-kit for type-safe schema storage.
16573
- * Action handlers are registered via addAction() and retrieved by name.
16574
+ * Validates that action handlers only contain allowed public methods
16575
+ * from the IPublicAction interface.
16576
+ *
16577
+ * Key features:
16578
+ * - Type-safe action schema registration
16579
+ * - Method name validation for class and object handlers
16580
+ * - Private method support (methods starting with _ or #)
16581
+ * - Schema override capabilities
16582
+ *
16583
+ * @example
16584
+ * ```typescript
16585
+ * // Register a class-based action
16586
+ * actionSchemaService.register("telegram-notifier", {
16587
+ * actionName: "telegram-notifier",
16588
+ * handler: TelegramNotifierAction,
16589
+ * callbacks: { ... }
16590
+ * });
16591
+ *
16592
+ * // Register an object-based action
16593
+ * actionSchemaService.register("logger", {
16594
+ * actionName: "logger",
16595
+ * handler: {
16596
+ * signal: async (event) => { ... },
16597
+ * dispose: async () => { ... }
16598
+ * }
16599
+ * });
16600
+ * ```
16574
16601
  */
16575
16602
  declare class ActionSchemaService {
16576
16603
  readonly loggerService: LoggerService;
@@ -16578,9 +16605,13 @@ declare class ActionSchemaService {
16578
16605
  /**
16579
16606
  * Registers a new action schema.
16580
16607
  *
16581
- * @param key - Unique action name
16582
- * @param value - Action schema configuration
16583
- * @throws Error if action name already exists
16608
+ * Validates the schema structure and method names before registration.
16609
+ * Throws an error if the action name already exists in the registry.
16610
+ *
16611
+ * @param key - Unique action name identifier
16612
+ * @param value - Action schema configuration with handler and optional callbacks
16613
+ * @throws Error if action name already exists in registry
16614
+ * @throws Error if validation fails (missing required fields, invalid handler, invalid method names)
16584
16615
  */
16585
16616
  register: (key: ActionName, value: IActionSchema) => void;
16586
16617
  /**
@@ -16588,28 +16619,36 @@ declare class ActionSchemaService {
16588
16619
  *
16589
16620
  * Performs shallow validation to ensure all required properties exist
16590
16621
  * and have correct types before registration in the registry.
16622
+ * Also validates that all public methods in the handler are allowed.
16591
16623
  *
16592
16624
  * @param actionSchema - Action schema to validate
16593
16625
  * @throws Error if actionName is missing or not a string
16594
- * @throws Error if handler is missing or not a function
16595
- * @throws Error if callbacks is not an object
16626
+ * @throws Error if handler is not a function or plain object
16627
+ * @throws Error if handler contains invalid public method names
16628
+ * @throws Error if callbacks is provided but not an object
16596
16629
  */
16597
16630
  private validateShallow;
16598
16631
  /**
16599
16632
  * Overrides an existing action schema with partial updates.
16600
16633
  *
16634
+ * Merges provided partial schema updates with the existing schema.
16635
+ * Useful for modifying handler or callbacks without re-registering the entire schema.
16636
+ *
16601
16637
  * @param key - Action name to override
16602
- * @param value - Partial schema updates
16603
- * @returns Updated action schema
16604
- * @throws Error if action name doesn't exist
16638
+ * @param value - Partial schema updates to merge
16639
+ * @returns Updated action schema after override
16640
+ * @throws Error if action name doesn't exist in registry
16605
16641
  */
16606
16642
  override: (key: ActionName, value: Partial<IActionSchema>) => IActionSchema;
16607
16643
  /**
16608
16644
  * Retrieves an action schema by name.
16609
16645
  *
16610
- * @param key - Action name
16646
+ * Returns the complete action schema configuration including handler and callbacks.
16647
+ * Used internally by ActionConnectionService to instantiate ClientAction instances.
16648
+ *
16649
+ * @param key - Action name identifier
16611
16650
  * @returns Action schema configuration
16612
- * @throws Error if action name doesn't exist
16651
+ * @throws Error if action name doesn't exist in registry
16613
16652
  */
16614
16653
  get: (key: ActionName) => IActionSchema;
16615
16654
  }