(function (root, factory) { "use strict"; const api = Object.freeze(factory()); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.MbnTradingHaltWorkflow = api; })(typeof globalThis === "object" ? globalThis : this, function () { "use strict"; const DEFAULT_FADE_DURATION = 6; const DEFAULT_MAXIMUM_RESULTS = 100; const MAXIMUM_RESULTS = 500; const MAXIMUM_QUERY_LENGTH = 64; const SCHEMA_VERSION = 1; const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/; const stockCodePattern = /^[A-Za-z0-9]{1,32}$/; const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u; const markets = Object.freeze(["kospi", "kosdaq"]); const bridgeContract = Object.freeze({ requestType: "search-trading-halts", resultType: "trading-halt-results", errorType: "trading-halt-error", defaultMaximumResults: DEFAULT_MAXIMUM_RESULTS, maximumResults: MAXIMUM_RESULTS, maximumQueryLength: MAXIMUM_QUERY_LENGTH }); const sourceContract = Object.freeze({ originalControl: "Control/UC7.cs", currentCutCode: "5001", candleCutCode: "8051", candlePeriodDays: 120, currentResolverGroups: Object.freeze({ kospi: "KOSPI", kosdaq: "KOSDAQ" }), candleResolverGroups: Object.freeze({ kospi: "KOSPI_STOCK", kosdaq: "KOSDAQ_STOCK" }) }); function action(id, label, builderKey, code, kind) { return Object.freeze({ id, label, builderKey, code, aliases: Object.freeze([code]), kind }); } const actions = Object.freeze([ action("current", "\uAC70\uB798\uC815\uC9C0-\uD604\uC7AC\uAC00", "s5001", "5001", "current"), action("candle-120d", "\uCE94\uB4E4\uADF8\uB798\uD504-\uAC70\uB798\uC815\uC9C0 ยท 120\uC77C", "s8010", "8051", "candle") ]); const actionsById = new Map(actions.map(value => [value.id, value])); function hasExactKeys(value, keys) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const actual = Object.keys(value).sort(); const expected = [...keys].sort(); return actual.length === expected.length && actual.every((key, index) => key === expected[index]); } function isSafeCanonicalText(value, maximumLength) { return typeof value === "string" && value.length > 0 && value.length <= maximumLength && value === value.trim() && !unsafeTextPattern.test(value); } function normalizeQuery(value) { if (typeof value !== "string" || value.length > MAXIMUM_QUERY_LENGTH || unsafeTextPattern.test(value)) { return null; } const query = value.trim(); return query.length <= MAXIMUM_QUERY_LENGTH && !unsafeTextPattern.test(query) ? query : null; } function normalizeTradingHalt(value) { if (!hasExactKeys(value, ["market", "stockCode", "displayName"]) || !markets.includes(value.market) || !isSafeCanonicalText(value.stockCode, 32) || !stockCodePattern.test(value.stockCode) || !isSafeCanonicalText(value.displayName, 200)) { return null; } return Object.freeze({ market: value.market, stockCode: value.stockCode, displayName: value.displayName }); } function createTradingHaltSearchRequest(requestId, query = "", maximumResults) { if (!isSafeCanonicalText(requestId, 128) || !identifierPattern.test(requestId)) { throw new TypeError("A safe trading-halt request id is required."); } const normalizedQuery = normalizeQuery(query); if (normalizedQuery === null) { throw new TypeError(`A trading-halt query must contain at most ${MAXIMUM_QUERY_LENGTH} safe characters.`); } if (maximumResults !== undefined && (!Number.isInteger(maximumResults) || maximumResults < 1 || maximumResults > MAXIMUM_RESULTS)) { throw new RangeError(`Trading-halt results must be limited from 1 through ${MAXIMUM_RESULTS}.`); } const request = { requestId, query: normalizedQuery }; if (maximumResults !== undefined) request.maximumResults = maximumResults; return Object.freeze(request); } function compareTradingHalts(left, right) { const marketOrder = markets.indexOf(left.market) - markets.indexOf(right.market); if (marketOrder !== 0) return marketOrder; if (left.displayName < right.displayName) return -1; if (left.displayName > right.displayName) return 1; if (left.stockCode < right.stockCode) return -1; if (left.stockCode > right.stockCode) return 1; return 0; } function tradingHaltIdentity(value) { const normalized = normalizeTradingHalt(value); return normalized ? `${normalized.market}\u0000${normalized.stockCode}` : ""; } function nextTradingHaltNameSort(direction) { return direction === "descending" ? "ascending" : "descending"; } function compareCanonicalText(left, right) { if (left < right) return -1; if (left > right) return 1; return 0; } function sortTradingHaltsForDisplay(values, direction) { if (!Array.isArray(values)) throw new TypeError("Trading-halt display rows are required."); if (direction !== null && direction !== "ascending" && direction !== "descending") { throw new TypeError("Trading-halt name sort must be ascending, descending, or null."); } if (direction === null) return [...values]; const nameDirection = direction === "ascending" ? 1 : -1; return values.map((value, sourceIndex) => { const normalized = normalizeTradingHalt(value); if (!normalized) throw new TypeError("A valid trading-halt display row is required."); return { value, normalized, sourceIndex }; }).sort((left, right) => { const nameOrder = compareCanonicalText( left.normalized.displayName, right.normalized.displayName); if (nameOrder !== 0) return nameOrder * nameDirection; const marketOrder = compareCanonicalText(left.normalized.market, right.normalized.market); if (marketOrder !== 0) return marketOrder; const stockCodeOrder = compareCanonicalText( left.normalized.stockCode, right.normalized.stockCode); return stockCodeOrder !== 0 ? stockCodeOrder : left.sourceIndex - right.sourceIndex; }).map(item => item.value); } function normalizeTradingHaltSearchResponse(value, expectedRequest) { if (!hasExactKeys(value, [ "requestId", "query", "retrievedAt", "totalRowCount", "truncated", "results" ]) || !isSafeCanonicalText(value.requestId, 128) || !identifierPattern.test(value.requestId) || normalizeQuery(value.query) !== value.query || !isSafeCanonicalText(value.retrievedAt, 64) || !Number.isFinite(Date.parse(value.retrievedAt)) || !Number.isInteger(value.totalRowCount) || value.totalRowCount < 0 || value.totalRowCount > MAXIMUM_RESULTS || typeof value.truncated !== "boolean" || !Array.isArray(value.results) || value.results.length !== value.totalRowCount) { return null; } if (expectedRequest !== undefined) { let expected; try { expected = createTradingHaltSearchRequest( expectedRequest?.requestId, expectedRequest?.query, expectedRequest?.maximumResults); } catch { return null; } if (value.requestId !== expected.requestId || value.query !== expected.query || (expected.maximumResults !== undefined && value.results.length > expected.maximumResults)) { return null; } } const results = []; const identities = new Set(); const lookupKeys = new Set(); for (const raw of value.results) { const item = normalizeTradingHalt(raw); if (!item) return null; const identity = `${item.market}\u0000${item.stockCode}`; const lookupKey = `${item.market}\u0000${item.displayName}`; if (identities.has(identity) || lookupKeys.has(lookupKey)) return null; if (results.length && compareTradingHalts(results[results.length - 1], item) > 0) return null; identities.add(identity); lookupKeys.add(lookupKey); results.push(item); } return Object.freeze({ requestId: value.requestId, query: value.query, retrievedAt: value.retrievedAt, totalRowCount: value.totalRowCount, truncated: value.truncated, results: Object.freeze(results) }); } function normalizeTradingHaltError(value, expectedRequest) { if (!hasExactKeys(value, ["requestId", "query", "message"]) || !isSafeCanonicalText(value.requestId, 128) || !identifierPattern.test(value.requestId) || normalizeQuery(value.query) !== value.query || !isSafeCanonicalText(value.message, 512)) { return null; } if (expectedRequest && (value.requestId !== expectedRequest.requestId || value.query !== normalizeQuery(expectedRequest.query))) { return null; } return Object.freeze({ requestId: value.requestId, query: value.query, message: value.message }); } function getAction(actionId) { return typeof actionId === "string" ? actionsById.get(actionId) || null : null; } function requireOptions(options) { if (!options || typeof options !== "object" || Array.isArray(options)) { throw new TypeError("Trading-halt playlist options are required."); } if (!isSafeCanonicalText(options.id, 128) || !identifierPattern.test(options.id)) { throw new TypeError("A safe trading-halt playlist id is required."); } const fadeDuration = options.fadeDuration === undefined ? DEFAULT_FADE_DURATION : options.fadeDuration; if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) { throw new RangeError("Fade duration must be an integer from 0 through 60."); } const ma5 = options.ma5 === undefined ? false : options.ma5; const ma20 = options.ma20 === undefined ? false : options.ma20; if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") { throw new TypeError("Moving-average flags must be boolean values."); } return Object.freeze({ id: options.id, fadeDuration, ma5, ma20 }); } function buildTradingHaltSelection(actionId, tradingHalt, options = {}) { const selectedAction = getAction(actionId); const selected = normalizeTradingHalt(tradingHalt); if (!selectedAction) throw new RangeError("The trading-halt action is unknown."); if (!selected) throw new TypeError("A valid trading-halt selection is required."); const ma5 = options.ma5 === undefined ? false : options.ma5; const ma20 = options.ma20 === undefined ? false : options.ma20; if (typeof ma5 !== "boolean" || typeof ma20 !== "boolean") { throw new TypeError("Moving-average flags must be boolean values."); } const movingAverages = Object.freeze({ ma5: selectedAction.kind === "candle" && ma5, ma20: selectedAction.kind === "candle" && ma20 }); const flags = []; if (movingAverages.ma5) flags.push("MA5"); if (movingAverages.ma20) flags.push("MA20"); const selection = selectedAction.kind === "current" ? Object.freeze({ groupCode: sourceContract.currentResolverGroups[selected.market], subject: selected.displayName, graphicType: "\u0031\uC5F4\uD310\uAE30\uBCF8", subtype: "HALTED", dataCode: selected.stockCode }) : Object.freeze({ groupCode: sourceContract.candleResolverGroups[selected.market], subject: selected.displayName, graphicType: flags.join(","), subtype: "TRADING_HALT", dataCode: selected.stockCode }); return Object.freeze({ builderKey: selectedAction.builderKey, code: selectedAction.code, aliases: selectedAction.aliases, selection, movingAverages }); } function createTradingHaltPlaylistEntry(tradingHalt, actionId, options) { const selected = normalizeTradingHalt(tradingHalt); if (!selected) throw new TypeError("A valid trading-halt selection is required."); const selectedAction = getAction(actionId); if (!selectedAction) throw new RangeError("The trading-halt action is unknown."); const normalizedOptions = requireOptions(options); const mapping = buildTradingHaltSelection(actionId, selected, normalizedOptions); return { id: normalizedOptions.id, builderKey: mapping.builderKey, code: mapping.code, aliases: [...mapping.aliases], title: selected.displayName, detail: selectedAction.label, category: selectedAction.kind === "candle" ? "chart" : "stock", market: selected.market, stockCode: selected.stockCode, displayName: selected.displayName, selection: mapping.selection, enabled: true, fadeDuration: normalizedOptions.fadeDuration, graphicType: mapping.selection.graphicType, subtype: mapping.selection.subtype, operator: Object.freeze({ source: "legacy-trading-halt-workflow", schemaVersion: SCHEMA_VERSION, actionId: selectedAction.id, tradingHalt: selected, movingAverages: mapping.movingAverages, resolverGroup: mapping.selection.groupCode }) }; } function sameStringArray(left, right) { return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => value === right[index]); } function sameSelection(left, right) { if (!left || typeof left !== "object" || !right || typeof right !== "object") return false; return ["groupCode", "subject", "graphicType", "subtype", "dataCode"] .every(key => typeof left[key] === "string" && left[key] === right[key]); } function restoreTradingHaltPlaylistEntry(value) { if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.enabled !== "boolean") { return null; } const operator = value.operator; if (!operator || typeof operator !== "object" || operator.source !== "legacy-trading-halt-workflow" || operator.schemaVersion !== SCHEMA_VERSION || !actionsById.has(operator.actionId) || !operator.movingAverages || typeof operator.movingAverages !== "object" || typeof operator.movingAverages.ma5 !== "boolean" || typeof operator.movingAverages.ma20 !== "boolean") { return null; } let recreated; try { recreated = createTradingHaltPlaylistEntry(operator.tradingHalt, operator.actionId, { id: value.id, fadeDuration: value.fadeDuration, ma5: operator.movingAverages.ma5, ma20: operator.movingAverages.ma20 }); } catch { return null; } const scalars = [ "id", "builderKey", "code", "title", "detail", "category", "market", "stockCode", "displayName", "fadeDuration", "graphicType", "subtype" ]; if (!scalars.every(key => value[key] === recreated[key]) || !sameStringArray(value.aliases, recreated.aliases) || !sameSelection(value.selection, recreated.selection) || operator.movingAverages.ma5 !== recreated.operator.movingAverages.ma5 || operator.movingAverages.ma20 !== recreated.operator.movingAverages.ma20 || operator.resolverGroup !== recreated.operator.resolverGroup) { return null; } return { ...recreated, enabled: value.enabled }; } return { bridgeContract, sourceContract, markets, actions, tradingHaltActions: actions, normalizeTradingHalt, normalizeTradingHaltResult: normalizeTradingHalt, tradingHaltIdentity, nextTradingHaltNameSort, sortTradingHaltsForDisplay, createTradingHaltSearchRequest, normalizeTradingHaltSearchResponse, normalizeTradingHaltError, getAction, buildTradingHaltSelection, createTradingHaltPlaylistEntry, restoreTradingHaltPlaylistEntry, refreshTradingHaltPlaylistEntry: restoreTradingHaltPlaylistEntry }; });