Files
alert/src/utils/symbolUtil.ts
2026-04-24 20:30:52 +08:00

326 lines
9.9 KiB
TypeScript

/**
* Transform flattened path data into a tree structure
* @param data Raw array returned by the backend
*/
export const transformToSymbolTree = (
data: API.SymbolDataItem[],
): API.SymbolTreeNode[] => {
const tree: API.SymbolTreeNode[] = [];
const map = new Map<string, API.SymbolTreeNode>();
data.forEach((item) => {
const parts = item.path.split('\\');
let currentPath = '';
parts.forEach((part, index) => {
const isLast = index === parts.length - 1;
const parentPath = currentPath;
currentPath = parentPath ? `${parentPath}\\${part}` : part;
if (!map.has(currentPath)) {
const newNode: API.SymbolTreeNode = {
title: part,
// Leaf nodes use symbol, directory nodes use path to prevent name conflicts
key: isLast ? item.symbol : currentPath,
};
if (isLast) {
newNode.value = item.symbol;
newNode.isLeaf = true;
newNode.meta = item;
} else {
newNode.children = [];
newNode.selectable = false; // Directory nodes are not selectable
}
map.set(currentPath, newNode);
// If there is no parent path, it is a root node
if (!parentPath) {
tree.push(newNode);
} else {
// Otherwise, add it to the parent's children
map.get(parentPath)?.children?.push(newNode);
}
}
});
});
return tree;
};
type SymbolTreeNode = {
title: string;
key: string;
value?: string;
path?: string;
children?: SymbolTreeNode[];
isLeaf?: boolean;
selectable?: boolean;
};
/**
* Generate a Pattern string based on the checkedKeys of Ant Design Tree
* @param treeData The tree structure of symbols (must contain complete symbol and key information)
* @param checkedKeys The checked keys of Ant Design Tree
* @param options Configuration items
*/
export function generatePattern(
treeData: SymbolTreeNode[],
checkedKeys: string[],
options: { useShortMode?: boolean; separator?: string } = {},
) {
const { useShortMode = true, separator = '\\' } = options;
const excludes: string[] = [];
const includes: string[] = [];
// --- Utility function: Get all leaf keys under a node ---
const getAllLeafKeys = (node: SymbolTreeNode): string[] => {
if (!node.children || node.children.length === 0) return [node.key];
return node.children.flatMap(getAllLeafKeys);
};
// --- Formatting function: process paths and wildcards ---
const format = (node: SymbolTreeNode, isExclude: boolean) => {
const isLeaf = !node.children || node.children.length === 0;
// Short mode (Logic from screenshot two): !*SYMBOL or path\*
if (useShortMode && isLeaf && node.value) {
return isExclude ? `!*${node.value}` : `*${node.value}`;
}
// Short mode still uses wildcards to match categories, but we want to keep the path structure for clarity
const path = node.key.split('/').join(separator); // Adapt to backslashes
const suffix = isLeaf ? '' : `${separator}*`;
const prefix = isExclude ? '!' : '';
// Special handling for exclusion logic in short mode:
// If it is short mode and we want to exclude a specific product, it should be !*SYMBOL according to screenshot two
if (useShortMode && isExclude && isLeaf && node.value) {
return `!*${node.value}`;
}
return `${prefix}${path}${suffix}`;
};
// --- Core recursive logic ---
const process = (node: SymbolTreeNode) => {
const allLeaves = getAllLeafKeys(node);
const selectedLeaves = allLeaves.filter((k) => checkedKeys.includes(k));
if (selectedLeaves.length === 0) return; // This branch is not selected at all
// 1. If the current level is fully selected
if (selectedLeaves.length === allLeaves.length) {
includes.push(format(node, false));
return;
}
// 2. If it is a leaf node (and is selected)
if (!node.children || node.children.length === 0) {
includes.push(format(node, false));
return;
}
if (selectedLeaves.length > allLeaves.length / 2) {
includes.push(format(node, false));
node.children.forEach((child) => {
const childLeaves = getAllLeafKeys(child);
const childSelected = childLeaves.filter((k) =>
checkedKeys.includes(k),
);
if (childSelected.length === 0) {
excludes.push(format(child, true));
} else if (childSelected.length < childLeaves.length) {
findExcludesOnly(child);
}
});
} else {
node.children.forEach(process);
}
};
const findExcludesOnly = (node: SymbolTreeNode) => {
const allLeaves = getAllLeafKeys(node);
const selectedLeaves = allLeaves.filter((k) => checkedKeys.includes(k));
if (selectedLeaves.length === 0) {
excludes.push(format(node, true));
return;
}
node.children?.forEach((child) => {
const childLeaves = getAllLeafKeys(child);
const childSelected = childLeaves.filter((k) => checkedKeys.includes(k));
if (childSelected.length === 0) {
excludes.push(format(child, true));
} else if (childSelected.length < childLeaves.length) {
findExcludesOnly(child);
}
});
};
treeData.forEach(process);
// 3. Final concatenation: Exclude items must be at the front
return [...excludes, ...includes].join(',');
}
/**
* Restore the background Pattern string to Ant Design Tree's checkedKeys
* @param patternString The string returned by the background, such as "!forex\exotic\*,forex\*"
* @param treeData Original tree structure (must contain complete symbol and key information)
* @param options Configuration items
*/
export function parsePatternToKeys(
patternString: string,
treeData: SymbolTreeNode[],
options: { separator?: string } = {},
): string[] {
if (!patternString) return [];
const { separator = '\\' } = options;
// 1. Split the string into rule array
const rules = patternString.split(',').map((s) => s.trim());
// 2. Get all leaf nodes (final products), because the selection state finally appears on the leaf nodes
const allLeafNodes: SymbolTreeNode[] = [];
const collectLeaves = (nodes: SymbolTreeNode[], parentPath: string = '') => {
nodes.forEach((node) => {
if (!node.children || node.children.length === 0) {
node.path = `${parentPath}${separator}${node.key}`;
allLeafNodes.push(node);
} else {
collectLeaves(node.children, node.key);
}
});
};
collectLeaves(treeData);
// 3. Define the matching logic (simulate the background matching process)
const isMatched = (
node: SymbolTreeNode,
rule: string,
): { matched: boolean; isExclude: boolean } => {
let tempRule = rule;
const isExclude = tempRule.startsWith('!');
if (isExclude) tempRule = tempRule.substring(1); // Remove !
let matched = false;
// Case A: Abbreviated pattern wildcard *BITCOIN
if (tempRule.startsWith('*')) {
const symbol = tempRule.substring(1);
matched = node.value === symbol;
}
// Case B: Path wildcard forex/general/*
else if (tempRule.endsWith('*')) {
const prefix = tempRule.substring(0, tempRule.length - 1);
// Check if the node's key starts with the prefix path
// Note: We need to handle separator consistency
const nodePath = (node?.path || node.key).split('/').join(separator);
matched = nodePath.startsWith(prefix);
}
// Case C: Exact path match forex/general/XAUUSD
else {
const nodePath = (node?.path || node.key).split('/').join(separator);
matched = nodePath === tempRule;
}
return { matched, isExclude };
};
// 4. Execute rule matching for each leaf node
const selectedKeys: string[] = [];
for (const node of allLeafNodes) {
// Simulate left-to-right matching, stop when hit
for (const rule of rules) {
const { matched, isExclude } = isMatched(node, rule);
if (matched) {
if (!isExclude && node.value) {
selectedKeys.push(node.value);
}
// Once a rule is hit (whether it's inclusion or exclusion), immediately break out of the current node's rule loop
break;
}
}
}
return selectedKeys;
}
export interface AntdTreeNode {
title: string;
value: string;
key?: string;
selectable?: boolean;
children?: AntdTreeNode[];
}
const CALC_MODE_MAP: Record<number, string> = {
0: 'Forex',
1: 'Futures',
2: 'CFD',
3: 'CFD Index',
4: 'CFD Leverage',
5: 'Forex No Leverage',
32: 'Exchange Stocks',
33: 'Exchange Futures',
34: 'Exchange FORTS Futures',
35: 'Exchange Options',
36: 'Exchange Options Margin',
37: 'Exchange Bonds',
38: 'Exchange MOEX Stocks',
39: 'Exchange MOEX Bonds',
64: 'Collateral',
};
export function transformToAssetsTree(
sourceData: API.SymbolDataItem[],
): AntdTreeNode[] {
const groupedData: Record<string, Set<string>> = {};
sourceData.forEach((item) => {
const { calcMode, currencyBase, currencyProfit, symbol } = item;
// Determine the category name, with 0 and 5 both classified as 'Forex'
let categoryName = CALC_MODE_MAP[calcMode] || 'Other';
if (calcMode === 0 || calcMode === 5) {
categoryName = 'Forex';
}
if (!groupedData[categoryName]) {
groupedData[categoryName] = new Set<string>();
}
if (calcMode === 0 || calcMode === 5) {
if (currencyBase) groupedData[categoryName].add(currencyBase);
if (currencyProfit) groupedData[categoryName].add(currencyProfit);
} else {
// Other cases extract the symbol directly to remove duplicates
if (symbol) groupedData[categoryName].add(symbol);
}
});
const treeData: AntdTreeNode[] = Object.keys(groupedData).map((category) => {
const uniqueValues = Array.from(groupedData[category]);
return {
title: category,
value: `GROUP_${category}`,
key: `GROUP_${category}`,
selectable: false,
children: uniqueValues.map((val) => ({
title: val,
value: val,
// key: `${category}_${val}`,
})),
};
});
return treeData;
}