528 lines
26 KiB
JavaScript
528 lines
26 KiB
JavaScript
(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
|
|
};
|
|
});
|