Complete legacy operator UI and playout migration
This commit is contained in:
527
Web/legacy-fixed-workflow.js
Normal file
527
Web/legacy-fixed-workflow.js
Normal file
@@ -0,0 +1,527 @@
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
|
||||
const api = Object.freeze(factory());
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
if (root) root.MbnLegacyFixedWorkflow = api;
|
||||
})(typeof globalThis === "object" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
const DEFAULT_FADE_DURATION = 6;
|
||||
const identifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
|
||||
const controlCharacterPattern = /[\u0000-\u001f\u007f]/;
|
||||
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const fixedMarkets = Object.freeze(["overseas", "exchange", "index"]);
|
||||
const runtimeAssets = Object.freeze({
|
||||
overseas: Object.freeze({
|
||||
relativePath: "bin/Debug/Res/해외.ini",
|
||||
sha256: "DBDC4EAC28BED9FD0DEABED7FE880F21CB49E48503FD3CCB95C0A29128818B2A"
|
||||
}),
|
||||
exchange: Object.freeze({
|
||||
relativePath: "bin/Debug/Res/환율.ini",
|
||||
sha256: "7A99F8DFB5DD8D8DB514605F232A0A7973545CA6A4A46ABE2D102ADD025B586C"
|
||||
}),
|
||||
index: Object.freeze({
|
||||
relativePath: "bin/Debug/Res/지수.ini",
|
||||
sha256: "E90ED317FD3ECC1A716AD17DD9BC21FE8B838574E1C20854BD0A886A96BA17E6"
|
||||
})
|
||||
});
|
||||
|
||||
const rawActions = [];
|
||||
|
||||
function cloneSelection(value) {
|
||||
return {
|
||||
groupCode: String(value.groupCode || ""),
|
||||
subject: String(value.subject || ""),
|
||||
graphicType: String(value.graphicType || ""),
|
||||
subtype: String(value.subtype || ""),
|
||||
dataCode: String(value.dataCode || "")
|
||||
};
|
||||
}
|
||||
|
||||
function addAction(market, section, label, definition) {
|
||||
const ordinal = rawActions.length + 1;
|
||||
rawActions.push({
|
||||
id: `fixed-${String(ordinal).padStart(3, "0")}`,
|
||||
market,
|
||||
section,
|
||||
label,
|
||||
detail: definition.detail || section,
|
||||
category: definition.category || "market",
|
||||
builderKey: definition.builderKey,
|
||||
code: definition.code,
|
||||
aliases: definition.aliases || [definition.code],
|
||||
available: definition.available !== false,
|
||||
prerequisite: definition.prerequisite || "",
|
||||
selection: cloneSelection(definition.selection || {}),
|
||||
dynamicDate: definition.dynamicDate === true,
|
||||
dynamicNxtSession: definition.dynamicNxtSession === true
|
||||
});
|
||||
}
|
||||
|
||||
const foreignIndices = [
|
||||
["다우", "Dow"], ["나스닥", "Nasdaq"], ["S&P", "Sp500"],
|
||||
["독일", "GermanyDax"], ["영국", "UnitedKingdomFtse"], ["프랑스", "FranceCac"],
|
||||
["니케이", "Nikkei"], ["중국 상해", "ShanghaiComposite"], ["홍콩 항셍", "HangSeng"],
|
||||
["대만 가권", "TaiwanWeighted"], ["싱가포르 지수", "SingaporeStraitsTimes"],
|
||||
["태국 지수", "ThailandSet"], ["필리핀 지수", "PhilippinesComposite"],
|
||||
["말레이시아 지수", "MalaysiaKlse"], ["인도네시아 지수", "IndonesiaComposite"]
|
||||
];
|
||||
const commodities = [
|
||||
["소맥", "FeedWheat"], ["옥수수", "Corn"], ["밀", "Wheat"], ["대두", "Soybean"],
|
||||
["콩", "Bean"], ["현미", "BrownRice"], ["커피", "Coffee"], ["코코아", "Cocoa"],
|
||||
["설탕", "Sugar"], ["생우(소)", "LiveCattle"], ["비육우", "FeederCattle"],
|
||||
["돈육(돼지)", "LeanHogs"], ["구리", "Copper"], ["철광석", "IronOre"],
|
||||
["니켈", "Nickel"], ["천연가스", "NaturalGas"], ["백금", "Platinum"],
|
||||
["팔라듐", "Palladium"], ["납", "Lead"], ["아연", "Zinc"], ["주석", "Tin"],
|
||||
["원면", "RawCotton"], ["목화(면화)", "Cotton"],
|
||||
["국제 금", "InternationalGold"], ["국제 은", "InternationalSilver"],
|
||||
["국내 금", "DomesticGold"], ["국내 은", "DomesticSilver"]
|
||||
];
|
||||
|
||||
for (const [label, subject] of foreignIndices) {
|
||||
addAction("overseas", "1열판기본", label, {
|
||||
builderKey: "s5001", code: "5001", category: "plate",
|
||||
selection: { groupCode: "FOREIGN_INDEX", subject, graphicType: "1열판기본", subtype: "CURRENT" }
|
||||
});
|
||||
}
|
||||
addAction("overseas", "1열판기본", "BDI 지수", {
|
||||
builderKey: "s5001", code: "5001", category: "plate",
|
||||
selection: { groupCode: "FOREIGN_INDEX", subject: "BALTIC_DRY_INDEX", graphicType: "1열판기본", subtype: "CURRENT" }
|
||||
});
|
||||
for (const [label, subject] of commodities) {
|
||||
addAction("overseas", "1열판기본", label, {
|
||||
builderKey: "s5001", code: "5001", category: "plate",
|
||||
selection: { groupCode: "COMMODITY", subject, graphicType: "1열판기본", subtype: "CURRENT" }
|
||||
});
|
||||
}
|
||||
|
||||
[
|
||||
["농산물[원면, 목화(면화)]", "Cotton"],
|
||||
["원자재[국제 금, 국제 은]", "InternationalPreciousMetals"],
|
||||
["원자재[국내 금, 국내 은]", "DomesticPreciousMetals"]
|
||||
].forEach(([label, subtype]) => addAction("overseas", "2열판-", label, {
|
||||
builderKey: "s50160", code: "50160", category: "plate",
|
||||
selection: { groupCode: "ALL", subject: "INDEX", graphicType: "2열판-", subtype }
|
||||
}));
|
||||
|
||||
[
|
||||
["미국 지수[다우, 나스닥, S&P]", "UnitedStatesIndices"],
|
||||
["중화권 지수[홍콩항셍, 상해종합, 대만]", "GreaterChinaIndices"],
|
||||
["유럽권 지수[영국, 프랑스, 독일]", "EuropeanIndices"],
|
||||
["아시아 지수[상해, 대만, 일본]", "AsianIndices"],
|
||||
["미국국채[2년, 3년, 5년]", "UnitedStatesTreasuries"],
|
||||
["채권금리(CD,CP,콜금리)", "BondYields"],
|
||||
["유가[두바이, 브렌트유, WTI]", "Oil"],
|
||||
["광물[금, 은, 구리]", "Minerals"],
|
||||
["식자재[대두, 옥수수, 밀]", "FoodMaterials"],
|
||||
["농산물[소맥, 옥수수, 밀]", "AgricultureWheatCorn"],
|
||||
["농산물[대두, 콩, 현미]", "AgricultureSoyRice"],
|
||||
["농산물[커피, 코코아, 설탕]", "AgricultureCoffeeCocoaSugar"],
|
||||
["농산물[생우(소), 비육우, 돈육(돼지)]", "AgricultureLivestock"],
|
||||
["원자재[구리, 철광석, 니켈]", "RawMaterialsCopperIronNickel"],
|
||||
["원자재[천연가스, 백금, 팔라듐]", "RawMaterialsGasPlatinumPalladium"],
|
||||
["원자재[납, 아연, 주석]", "RawMaterialsLeadZincTin"]
|
||||
].forEach(([label, subtype]) => addAction("overseas", "3열판", label, {
|
||||
builderKey: "s5016", code: "5016", category: "plate",
|
||||
selection: { groupCode: "ALL", subject: "INDEX", graphicType: "3열판", subtype }
|
||||
}));
|
||||
|
||||
[
|
||||
["글로벌 증시 지도", "8067"], ["아시아 증시 지도", "5072"],
|
||||
["미국 증시 지도", "5068"], ["유럽 증시 지도", "5070"]
|
||||
].forEach(([label, code]) => addAction("overseas", "글로벌 증시 지도", label, {
|
||||
builderKey: "s8067", code, aliases: [code],
|
||||
selection: { groupCode: "ALL", subject: "INDEX", graphicType: "WORLD_MAP", subtype: code }
|
||||
}));
|
||||
|
||||
[
|
||||
["미국 다우 지수", "DowJones"], ["미국 나스닥 지수", "Nasdaq"],
|
||||
["미국 S&P 지수", "StandardAndPoor500"], ["중국 상해 지수", "China"],
|
||||
["일본 닛케이 지수", "Japan"], ["유가 두바이 지수", "DubaiCrude"],
|
||||
["유가 WTI 지수", "WtiCrude"], ["유가 브렌트유 지수", "BrentCrude"],
|
||||
["금 지수", "Gold"]
|
||||
].forEach(([label, subtype]) => addAction("overseas", "이미지 그래프", label, {
|
||||
builderKey: "s6001", code: "6001",
|
||||
selection: { groupCode: "FOREIGN_INDEX", subject: label, graphicType: "IMAGE_GRAPH", subtype }
|
||||
}));
|
||||
|
||||
[["다우", "DOW"], ["나스닥", "NASDAQ"], ["S&P", "S&P500"]]
|
||||
.forEach(([label, subject]) => addAction("overseas", "미국업종", label, {
|
||||
builderKey: "s5078", code: "5078", category: "chart",
|
||||
selection: { groupCode: "FOREIGN_INDEX", subject, graphicType: "US_SECTOR_INDEX", subtype: "" }
|
||||
}));
|
||||
|
||||
const exchangeTargets = [
|
||||
["원달러", "WonDollar"], ["원엔", "WonYen"],
|
||||
["원위엔", "WonYuan"], ["원유로", "WonEuro"]
|
||||
];
|
||||
for (const [label, subject] of exchangeTargets) {
|
||||
addAction("exchange", "1열판기본", label, {
|
||||
builderKey: "s5001", code: "5001", category: "plate",
|
||||
selection: { groupCode: "EXCHANGE_RATE", subject, graphicType: "1열판기본", subtype: "CURRENT" }
|
||||
});
|
||||
}
|
||||
[
|
||||
["해외환율[엔달러, 위안화달러, 유로달러]", "OverseasExchangeRates"],
|
||||
["환율[원달러, 원엔, 원유로]", "DomesticExchangeRates"]
|
||||
].forEach(([label, subtype]) => addAction("exchange", "3열판", label, {
|
||||
builderKey: "s5016", code: "5016", category: "plate",
|
||||
selection: { groupCode: "ALL", subject: "INDEX", graphicType: "3열판", subtype }
|
||||
}));
|
||||
|
||||
const yieldPeriods = [
|
||||
["5일", "FiveDays"], ["20일", "TwentyDays"], ["60일", "SixtyDays"],
|
||||
["120일", "OneHundredTwentyDays"], ["240일", "TwoHundredFortyDays"]
|
||||
];
|
||||
for (const [label, subject] of exchangeTargets) {
|
||||
for (const [periodLabel, subtype] of yieldPeriods) {
|
||||
addAction("exchange", "수익률 그래프", `${label}_${periodLabel}`, {
|
||||
builderKey: "s5086", code: "5086", category: "chart",
|
||||
selection: { groupCode: "EXCHANGE_RATE", subject, graphicType: "YIELD_GRAPH", subtype }
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const [label, subject] of exchangeTargets) {
|
||||
for (const [periodLabel, subtype] of yieldPeriods) {
|
||||
addAction("exchange", "라인 그래프", `${label}_${periodLabel}`, {
|
||||
builderKey: "s50860", code: "50860", category: "chart",
|
||||
selection: { groupCode: "EXCHANGE_RATE", subject, graphicType: "LINE_GRAPH", subtype }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[
|
||||
["3열판_주요지수[코스피, 코스닥, 코스피200]", "MajorKospiKosdaqKospi200"],
|
||||
["3열판_주요지수[코스피, 코스닥, 선물]", "MajorKospiKosdaqFutures"],
|
||||
["3열판_예상체결지수[코스피, 코스닥, 코스피200]", "ExpectedKospiKosdaqKospi200"],
|
||||
["3열판_예상체결지수[코스피, 코스닥, 선물]", "ExpectedKospiKosdaqFutures"]
|
||||
].forEach(([label, subtype]) => addAction("index", "주요지수", label, {
|
||||
builderKey: "s5016", code: "5016", category: "plate",
|
||||
selection: { groupCode: "ALL", subject: "INDEX", graphicType: "3열판", subtype }
|
||||
}));
|
||||
|
||||
const candlePeriods = [
|
||||
["일봉", "8035"], ["5일", "8061"], ["20일", "8040"],
|
||||
["60일", "8046"], ["120일", "8051"], ["240일", "8056"]
|
||||
];
|
||||
function addIndexMarket(section, groupCode, candleGroup, options) {
|
||||
addAction("index", section, "1열판기본_예상지수", {
|
||||
builderKey: "s5001", code: "5001", category: "plate",
|
||||
selection: { groupCode, subject: "INDEX", graphicType: "1열판기본", subtype: "EXPECTED_INDEX" }
|
||||
});
|
||||
addAction("index", section, "1열판기본_현재지수", {
|
||||
builderKey: "s5001", code: "5001", category: "plate",
|
||||
selection: { groupCode, subject: "INDEX", graphicType: "1열판기본", subtype: "CURRENT" }
|
||||
});
|
||||
if (options.yield) {
|
||||
for (const [periodLabel, subtype] of yieldPeriods) {
|
||||
addAction("index", section, `수익률그래프_${periodLabel}`, {
|
||||
builderKey: "s5086", code: "5086", category: "chart",
|
||||
selection: { groupCode, subject: "INDEX", graphicType: "YIELD_GRAPH", subtype }
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const [periodLabel, code] of candlePeriods) {
|
||||
addAction("index", section, `캔들그래프_${periodLabel}`, {
|
||||
builderKey: "s8010", code, category: "chart",
|
||||
selection: { groupCode: candleGroup, subject: "INDEX", graphicType: "", subtype: "PRICE" }
|
||||
});
|
||||
}
|
||||
if (options.volume) {
|
||||
for (const [periodLabel, code] of candlePeriods) {
|
||||
addAction("index", section, `캔들그래프(거래량)_${periodLabel}`, {
|
||||
builderKey: "s8010", code, category: "chart",
|
||||
selection: { groupCode: candleGroup, subject: "INDEX", graphicType: "", subtype: "VOLUME" }
|
||||
});
|
||||
}
|
||||
}
|
||||
if (options.expected) {
|
||||
for (const [periodLabel, code] of candlePeriods.slice(1)) {
|
||||
addAction("index", section, `캔들그래프(예상지수)_${periodLabel}`, {
|
||||
builderKey: "s8010", code, category: "chart",
|
||||
selection: { groupCode: candleGroup, subject: "INDEX", graphicType: "", subtype: "EXPECTED" }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
addIndexMarket("코스피", "KOSPI", "KOSPI_INDEX", { yield: true, volume: true, expected: true });
|
||||
addIndexMarket("코스닥", "KOSDAQ", "KOSDAQ_INDEX", { yield: true, volume: true, expected: true });
|
||||
addIndexMarket("코스피200", "KOSPI200", "KOSPI200_INDEX", { yield: true, volume: true, expected: true });
|
||||
addIndexMarket("선물", "FUTURES", "FUTURES_INDEX", { yield: false, volume: false, expected: true });
|
||||
addIndexMarket("KRX100", "KRX100", "KRX100_INDEX", { yield: true, volume: true, expected: false });
|
||||
|
||||
function trend(label, builderKey, code, groupCode, graphicType, subtype, available = true) {
|
||||
addAction("index", "매매동향", label, {
|
||||
builderKey, code, available, category: "market",
|
||||
prerequisite: available ? "" : "원본 수동 순매도 데이터 파일과 검증된 입력 화면이 필요합니다.",
|
||||
selection: { groupCode, subject: "INDEX", graphicType, subtype }
|
||||
});
|
||||
}
|
||||
trend("주체별 매매동향_표그래프", "s5082", "5082", "ALL", "TRADING_BY_PARTICIPANT", "TABLE_GRAPH");
|
||||
trend("코스피 매매동향_표그래프", "s5023", "5023", "KOSPI", "KOSPI_TRADING_TREND", "TABLE_GRAPH");
|
||||
trend("코스피 매매동향_라인그래프", "s5084", "5084", "KOSPI", "KOSPI_TRADING_TREND", "LINE_GRAPH");
|
||||
trend("코스피 매매동향_막대그래프", "s5024", "5024", "KOSPI", "KOSPI_TRADING_TREND", "BAR_GRAPH");
|
||||
trend("코스닥 매매동향_표그래프", "s5023", "5023", "KOSDAQ", "KOSDAQ_TRADING_TREND", "TABLE_GRAPH");
|
||||
trend("코스닥 매매동향_라인그래프", "s5084", "5084", "KOSDAQ", "KOSDAQ_TRADING_TREND", "LINE_GRAPH");
|
||||
trend("코스닥 매매동향_막대그래프", "s5024", "5024", "KOSDAQ", "KOSDAQ_TRADING_TREND", "BAR_GRAPH");
|
||||
trend("개인 매매동향_라인그래프", "s5083", "5083", "ALL", "INDIVIDUAL_TRADING_TREND", "LINE_GRAPH");
|
||||
trend("외국인 매매동향_라인그래프", "s5083", "5083", "ALL", "FOREIGN_TRADING_TREND", "LINE_GRAPH");
|
||||
trend("기관 매매동향_라인그래프", "s5083", "5083", "ALL", "INSTITUTION_TRADING_TREND", "LINE_GRAPH");
|
||||
trend("기관 순매수 현황_표그래프", "s6067", "6067", "ALL", "INSTITUTION_NET_BUY", "TABLE_GRAPH");
|
||||
trend("프로그램 매매_표그래프", "s5085", "5085", "ALL", "PROGRAM_TRADING", "TABLE_GRAPH");
|
||||
trend("개인 순매도 상위(수동)", "s5025", "5025", "INDIVIDUAL", "MANUAL_NET_SELL", "MANUAL_NET_SELL", false);
|
||||
trend("외국인 순매도 상위(수동)", "s5025", "5025", "FOREIGN", "MANUAL_NET_SELL", "MANUAL_NET_SELL", false);
|
||||
trend("기관 순매도 상위(수동)", "s5025", "5025", "INSTITUTION", "MANUAL_NET_SELL", "MANUAL_NET_SELL", false);
|
||||
|
||||
const fiveRows = [
|
||||
"코스피 시가총액", "코스피 예상가 시가총액 상위", "코스피 상승률 상위", "코스피 하락률 상위",
|
||||
"코스피 시간외 상승률 상위", "코스피 시간외 하락률 상위",
|
||||
"코스닥 시가총액", "코스닥 예상가 시가총액 상위", "코스닥 상승률 상위", "코스닥 하락률 상위",
|
||||
"코스닥 시간외 상승률 상위", "코스닥 시간외 하락률 상위", "코스피 200 종목", "ETF 종목", "VI 발동(수동)",
|
||||
"코스피_NXT 시가총액", "코스피_NXT 상승률 상위", "코스피_NXT 하락률 상위",
|
||||
"코스피_NXT 시간외 상승률 상위", "코스피_NXT 시간외 하락률 상위", "코스피_NXT 거래량 상위",
|
||||
"코스닥_NXT 시가총액", "코스닥_NXT 상승률 상위", "코스닥_NXT 하락률 상위",
|
||||
"코스닥_NXT 시간외 상승률 상위", "코스닥_NXT 시간외 하락률 상위", "코스닥_NXT 거래량 상위"
|
||||
];
|
||||
const sixAndTwelveRows = [
|
||||
"코스피 시가총액", "코스피 예상가 시가총액 상위", "코스피 상승률 상위", "코스피 하락률 상위",
|
||||
"코스피 52주 신고가", "코스피 52주 신저가", "코스피 시간외 상승률 상위", "코스피 시간외 하락률 상위",
|
||||
"코스닥 시가총액", "코스닥 예상가 시가총액 상위", "코스닥 상승률 상위", "코스닥 하락률 상위",
|
||||
"코스닥 52주 신고가", "코스닥 52주 신저가", "코스닥 시간외 상승률 상위", "코스닥 시간외 하락률 상위",
|
||||
"코스피 200 종목", "ETF 종목", "VI 발동(수동)",
|
||||
"코스피_NXT 시가총액", "코스피_NXT 상승률 상위", "코스피_NXT 하락률 상위", "코스피_NXT 거래량 상위",
|
||||
"코스닥_NXT 시가총액", "코스닥_NXT 상승률 상위", "코스닥_NXT 하락률 상위", "코스닥_NXT 거래량 상위"
|
||||
];
|
||||
|
||||
function pagedDefinition(label, builderKey, code) {
|
||||
if (label === "VI 발동(수동)") {
|
||||
return {
|
||||
// VILIST removes the placeholder row and always creates the legacy
|
||||
// five-row outcome, even when the placeholder came from the 6/12 section.
|
||||
builderKey: "s5074", code: "5074", available: false, category: "plate",
|
||||
prerequisite: "검증된 VI 종목 목록 입력 화면이 필요합니다.",
|
||||
selection: { groupCode: "PAGED_VI", subject: "", graphicType: "INPUT_ORDER", subtype: "CURRENT" }
|
||||
};
|
||||
}
|
||||
let groupCode;
|
||||
if (label.startsWith("코스피_NXT")) groupCode = "PAGED_NXT_KOSPI";
|
||||
else if (label.startsWith("코스닥_NXT")) groupCode = "PAGED_NXT_KOSDAQ";
|
||||
else if (label.startsWith("코스닥")) groupCode = "PAGED_DOMESTIC_KOSDAQ";
|
||||
else groupCode = "PAGED_DOMESTIC_KOSPI";
|
||||
|
||||
let subtype;
|
||||
if (label.includes("예상가 시가총액")) subtype = "EXPECTED_MARKET_CAP";
|
||||
else if (label.includes("시가총액")) subtype = "MARKET_CAP";
|
||||
else if (label.includes("시간외 상승률")) subtype = "AFTER_HOURS_GAIN_RATE";
|
||||
else if (label.includes("시간외 하락률")) subtype = "AFTER_HOURS_LOSS_RATE";
|
||||
else if (label.includes("상승률")) subtype = "GAIN_RATE";
|
||||
else if (label.includes("하락률")) subtype = "LOSS_RATE";
|
||||
else if (label.includes("52주 신고가")) subtype = "52_WEEK_HIGH";
|
||||
else if (label.includes("52주 신저가")) subtype = "52_WEEK_LOW";
|
||||
else if (label.includes("200 종목")) subtype = "KOSPI200";
|
||||
else if (label.startsWith("ETF")) subtype = "ETF";
|
||||
else if (label.includes("거래량")) subtype = "VOLUME";
|
||||
else throw new Error(`Unknown paged legacy action: ${label}`);
|
||||
|
||||
const dynamicNxtSession = groupCode.startsWith("PAGED_NXT_");
|
||||
return {
|
||||
builderKey, code, category: "plate",
|
||||
dynamicDate: !dynamicNxtSession,
|
||||
dynamicNxtSession,
|
||||
selection: {
|
||||
groupCode,
|
||||
subject: "INDEX",
|
||||
graphicType: dynamicNxtSession ? "$NXT_SESSION" : "",
|
||||
subtype,
|
||||
dataCode: dynamicNxtSession ? "" : "$TODAY"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function addPagedSection(section, labels, builderKey, code) {
|
||||
for (const label of labels) addAction("index", section, label, pagedDefinition(label, builderKey, code));
|
||||
}
|
||||
addPagedSection("5단 표그래프", fiveRows, "s5074", "5074");
|
||||
addPagedSection("6종목 현재가", sixAndTwelveRows, "s5077", "5077");
|
||||
addPagedSection("12종목 현재가", sixAndTwelveRows, "s5088", "5088");
|
||||
|
||||
const actions = Object.freeze(rawActions.map(value => Object.freeze({
|
||||
...value,
|
||||
aliases: Object.freeze([...value.aliases]),
|
||||
selection: Object.freeze({ ...value.selection })
|
||||
})));
|
||||
if (actions.length !== 328) throw new Error("The three fixed legacy tabs must contain exactly 328 actions.");
|
||||
if (actions.filter(action => action.available).length !== 322) {
|
||||
throw new Error("Exactly six fixed-tab actions must remain trusted manual prerequisites.");
|
||||
}
|
||||
const byId = new Map(actions.map(action => [action.id, action]));
|
||||
|
||||
function localDateText(now = new Date()) {
|
||||
const year = String(now.getFullYear()).padStart(4, "0");
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function currentNxtSession(now = new Date()) {
|
||||
return now.getHours() <= 12 ? "PRE_MARKET" : "AFTER_MARKET";
|
||||
}
|
||||
|
||||
function materializeSelection(action, now, movingAverages) {
|
||||
const selection = cloneSelection(action.selection);
|
||||
if (action.dynamicDate) selection.dataCode = localDateText(now);
|
||||
if (action.dynamicNxtSession) selection.graphicType = currentNxtSession(now);
|
||||
if (action.builderKey === "s8010") {
|
||||
const flags = [];
|
||||
if (movingAverages.ma5) flags.push("MA5");
|
||||
if (movingAverages.ma20) flags.push("MA20");
|
||||
selection.graphicType = flags.join(",");
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
function requireOptions(options) {
|
||||
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
||||
throw new TypeError("Fixed playlist options are required.");
|
||||
}
|
||||
const id = typeof options.id === "string" ? options.id.trim() : "";
|
||||
if (!identifierPattern.test(id)) throw new TypeError("A safe fixed playlist entry id is required.");
|
||||
const fadeDuration = options.fadeDuration === undefined ? DEFAULT_FADE_DURATION : Number(options.fadeDuration);
|
||||
if (!Number.isInteger(fadeDuration) || fadeDuration < 0 || fadeDuration > 60) {
|
||||
throw new RangeError("Fade duration must be an integer from 0 through 60.");
|
||||
}
|
||||
const now = options.now instanceof Date && !Number.isNaN(options.now.valueOf()) ? options.now : new Date();
|
||||
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 { id, fadeDuration, now, ma5, ma20 };
|
||||
}
|
||||
|
||||
function createFixedPlaylistEntry(actionId, options) {
|
||||
const action = typeof actionId === "string" ? byId.get(actionId) : null;
|
||||
if (!action) throw new RangeError("The selected fixed legacy action is unknown.");
|
||||
if (!action.available) throw new RangeError(action.prerequisite || "The selected action requires trusted manual data.");
|
||||
const normalized = requireOptions(options);
|
||||
const movingAverages = Object.freeze({
|
||||
ma5: action.builderKey === "s8010" && normalized.ma5,
|
||||
ma20: action.builderKey === "s8010" && normalized.ma20
|
||||
});
|
||||
const selection = Object.freeze(materializeSelection(action, normalized.now, movingAverages));
|
||||
return {
|
||||
id: normalized.id,
|
||||
builderKey: action.builderKey,
|
||||
code: action.code,
|
||||
aliases: [...action.aliases],
|
||||
title: action.label,
|
||||
detail: `${action.section} · ${action.label}`,
|
||||
category: action.category,
|
||||
market: action.market,
|
||||
selection,
|
||||
enabled: true,
|
||||
fadeDuration: normalized.fadeDuration,
|
||||
graphicType: selection.graphicType,
|
||||
subtype: selection.subtype,
|
||||
operator: Object.freeze({
|
||||
source: "legacy-fixed-workflow",
|
||||
actionId: action.id,
|
||||
legacyTab: action.market,
|
||||
legacySection: action.section,
|
||||
legacyLabel: action.label,
|
||||
assetPath: runtimeAssets[action.market].relativePath,
|
||||
assetSha256: runtimeAssets[action.market].sha256,
|
||||
movingAverages
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function sameStringArray(left, right) {
|
||||
return Array.isArray(left) && Array.isArray(right) &&
|
||||
left.length === right.length && left.every((value, index) => value === right[index]);
|
||||
}
|
||||
|
||||
function normalizeStoredMovingAverages(value, action) {
|
||||
const stored = value?.operator?.movingAverages;
|
||||
if (stored !== undefined) {
|
||||
if (!stored || typeof stored !== "object" || Array.isArray(stored) ||
|
||||
typeof stored.ma5 !== "boolean" || typeof stored.ma20 !== "boolean" ||
|
||||
Object.keys(stored).length !== 2) return null;
|
||||
if (action.builderKey !== "s8010" && (stored.ma5 || stored.ma20)) return null;
|
||||
return Object.freeze({ ma5: stored.ma5, ma20: stored.ma20 });
|
||||
}
|
||||
if (action.builderKey !== "s8010") return Object.freeze({ ma5: false, ma20: false });
|
||||
const graphicType = value?.selection?.graphicType;
|
||||
if (!["", "MA5", "MA20", "MA5,MA20"].includes(graphicType)) return null;
|
||||
return Object.freeze({
|
||||
ma5: graphicType === "MA5" || graphicType === "MA5,MA20",
|
||||
ma20: graphicType === "MA20" || graphicType === "MA5,MA20"
|
||||
});
|
||||
}
|
||||
|
||||
function storedSelectionMatches(value, action, movingAverages) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
for (const key of ["groupCode", "subject", "graphicType", "subtype", "dataCode"]) {
|
||||
const text = value[key];
|
||||
if (typeof text !== "string" || text.length > 256 || controlCharacterPattern.test(text)) return false;
|
||||
if (action.dynamicDate && key === "dataCode") {
|
||||
if (!datePattern.test(text)) return false;
|
||||
} else if (action.dynamicNxtSession && key === "graphicType") {
|
||||
if (!(["PRE_MARKET", "AFTER_MARKET"].includes(text))) return false;
|
||||
} else if (action.builderKey === "s8010" && key === "graphicType") {
|
||||
const flags = [];
|
||||
if (movingAverages.ma5) flags.push("MA5");
|
||||
if (movingAverages.ma20) flags.push("MA20");
|
||||
if (text !== flags.join(",")) return false;
|
||||
} else if (text !== action.selection[key]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function restoreFixedPlaylistEntry(value, options = {}) {
|
||||
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-fixed-workflow") return null;
|
||||
const action = byId.get(operator.actionId);
|
||||
const movingAverages = action ? normalizeStoredMovingAverages(value, action) : null;
|
||||
if (!action || !action.available || !movingAverages ||
|
||||
!storedSelectionMatches(value.selection, action, movingAverages)) return null;
|
||||
if (operator.legacyTab !== action.market || operator.legacySection !== action.section ||
|
||||
operator.legacyLabel !== action.label || operator.assetPath !== runtimeAssets[action.market].relativePath ||
|
||||
operator.assetSha256 !== runtimeAssets[action.market].sha256) return null;
|
||||
if (value.builderKey !== action.builderKey || value.code !== action.code ||
|
||||
value.title !== action.label || value.detail !== `${action.section} · ${action.label}` ||
|
||||
value.category !== action.category || value.market !== action.market ||
|
||||
!sameStringArray(value.aliases, action.aliases)) return null;
|
||||
let recreated;
|
||||
try {
|
||||
recreated = createFixedPlaylistEntry(action.id, {
|
||||
id: value.id,
|
||||
fadeDuration: value.fadeDuration,
|
||||
now: options.now,
|
||||
ma5: movingAverages.ma5,
|
||||
ma20: movingAverages.ma20
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return { ...recreated, enabled: value.enabled };
|
||||
}
|
||||
|
||||
function actionsForMarket(market) {
|
||||
return fixedMarkets.includes(market) ? actions.filter(action => action.market === market) : [];
|
||||
}
|
||||
|
||||
return {
|
||||
runtimeAssets,
|
||||
fixedMarkets,
|
||||
actions,
|
||||
actionsForMarket,
|
||||
localDateText,
|
||||
currentNxtSession,
|
||||
createFixedPlaylistEntry,
|
||||
restoreFixedPlaylistEntry,
|
||||
refreshFixedPlaylistEntry: restoreFixedPlaylistEntry
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user