Skip to main content
Glama
emerzon

MetaTrader5 MCP Server

by emerzon
VolatilityControls.tsx12 kB
import { useEffect, useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { forecastVolatility, getVolatilityMethods } from '../api/client' import type { VolatilityMethodsMeta } from '../types' import { loadJSON, saveJSON } from '../lib/storage' type Props = { symbol?: string timeframe: string anchor?: number onResult: (res: any) => void } type ParamDef = { name: string type: string default?: any description?: string } type ParamsMap = Record<string, Record<string, any>> function buildDefaults(meta?: VolatilityMethodsMeta['methods'][number]): Record<string, any> { if (!meta) return {} const out: Record<string, any> = {} for (const param of meta.params || []) { if (param.default !== undefined && param.default !== null) { out[param.name] = param.default } } return out } function stringifyValue(value: any, type?: string): string { if (value === undefined || value === null) return '' const lower = type?.toLowerCase() ?? '' if (typeof value === 'boolean') return value ? 'true' : 'false' if (Array.isArray(value)) return value.join(', ') if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '' if (lower.startsWith('json') && typeof value === 'object') { try { return JSON.stringify(value) } catch { return '' } } return String(value) } function parseParamValue(input: string, type?: string) { const raw = input.trim() if (!raw) { return '' } const t = (type || '').toLowerCase() if (t === 'int') { const v = Number.parseInt(raw, 10) return Number.isNaN(v) ? raw : v } if (t === 'float') { const v = Number.parseFloat(raw) return Number.isNaN(v) ? raw : v } if (t === 'bool') { if (raw === 'true' || raw === '1') return true if (raw === 'false' || raw === '0') return false return raw } if (t.startsWith('list')) { const inner = t.includes('[') ? t.slice(t.indexOf('[') + 1, t.lastIndexOf(']')).trim() : 'str' const parts = raw.split(/[\s,]+/).map((p) => p.trim()).filter(Boolean) if (inner === 'int') { const vals = parts.map((p) => Number.parseInt(p, 10)).filter((v) => !Number.isNaN(v)) return vals } if (inner === 'float') { const vals = parts.map((p) => Number.parseFloat(p)).filter((v) => !Number.isNaN(v)) return vals } return parts } if (t === 'json') { try { return JSON.parse(raw) } catch { return raw } } return raw } function isEmptyParamValue(value: any): boolean { if (value === '' || value === undefined || value === null) return true if (Array.isArray(value)) return value.length === 0 return false } export function VolatilityControls({ symbol, timeframe, anchor, onResult }: Props) { const { data: methodsData, refetch: refetchMethods } = useQuery({ queryKey: ['volMethods'], queryFn: getVolatilityMethods, }) const methods = methodsData?.methods ?? [] const [method, setMethod] = useState<string>('') const [horizon, setHorizon] = useState<number>(12) const [proxy, setProxy] = useState<string>('squared_return') const [paramsByMethod, setParamsByMethod] = useState<ParamsMap>({}) const storageKey = symbol && timeframe ? `vol:${symbol}:${timeframe}` : null // Load saved preferences when symbol/timeframe changes useEffect(() => { if (!storageKey) { setMethod('') setParamsByMethod({}) setHorizon(12) setProxy('squared_return') return } const saved = loadJSON<any>(storageKey) if (saved) { if (typeof saved.method === 'string') setMethod(saved.method) if (typeof saved.horizon === 'number') setHorizon(saved.horizon) if (typeof saved.proxy === 'string') setProxy(saved.proxy) if (saved.paramsByMethod && typeof saved.paramsByMethod === 'object') { setParamsByMethod(saved.paramsByMethod as ParamsMap) } else { setParamsByMethod({}) } } else { setMethod('') setParamsByMethod({}) setHorizon(12) setProxy('squared_return') } }, [storageKey]) // Ensure a valid method is selected once metadata arrives useEffect(() => { if (!methods.length) return setMethod((prev) => { if (prev && methods.some((m) => m.method === prev)) return prev const preferred = methods.find((m) => m.available)?.method ?? methods[0].method return preferred }) }, [methods]) const selectedMeta = useMemo(() => methods.find((m) => m.method === method), [methods, method]) // Seed defaults for the active method if absent useEffect(() => { if (!method || !selectedMeta) return setParamsByMethod((prev) => { if (prev[method]) return prev const defaults = buildDefaults(selectedMeta) if (Object.keys(defaults).length === 0) { return prev } return { ...prev, [method]: defaults } }) }, [method, selectedMeta]) // Persist preferences useEffect(() => { if (!storageKey || !method) return const payload = { method, horizon, proxy, paramsByMethod, } saveJSON(storageKey, payload) }, [storageKey, method, horizon, proxy, paramsByMethod]) const activeParams = paramsByMethod[method] ?? {} const advancedSummary = useMemo(() => { if (!selectedMeta) return 'None' const entries: string[] = [] for (const param of selectedMeta.params || []) { const value = activeParams[param.name] if (isEmptyParamValue(value)) continue if (Array.isArray(value)) { entries.push(`${param.name}=[${value.join(', ')}]`) } else { entries.push(`${param.name}=${value}`) } } return entries.length ? entries.join(' | ') : 'Defaults' }, [selectedMeta, activeParams]) function updateParam(name: string, rawValue: string, param: ParamDef) { setParamsByMethod((prev) => { const current = { ...(prev[method] ?? {}) } const parsed = parseParamValue(rawValue, param.type) if (isEmptyParamValue(parsed)) { delete current[name] } else { current[name] = parsed } return { ...prev, [method]: current } }) } function toggleBoolean(name: string, value: boolean) { setParamsByMethod((prev) => { const current = { ...(prev[method] ?? {}) } current[name] = value return { ...prev, [method]: current } }) } const canRun = Boolean(symbol) && selectedMeta?.available !== false async function run() { if (!symbol || !selectedMeta) return const paramsRaw = paramsByMethod[method] ?? {} const params: Record<string, any> = {} for (const [key, value] of Object.entries(paramsRaw)) { if (isEmptyParamValue(value)) continue params[key] = value } const body = { symbol, timeframe, method, horizon, proxy, params, as_of: anchor && !Number.isNaN(anchor) ? new Date(anchor * 1000).toISOString().slice(0, 19).replace('T', ' ') : undefined, } const res = await forecastVolatility(body) onResult(res) } return ( <div className="flex flex-col gap-3"> <div className="flex flex-wrap items-end gap-3"> <label className="flex flex-col"> <span className="label">Method</span> <div className="flex gap-2 items-center"> <select className="select w-48" value={method} onChange={(e) => setMethod(e.target.value)}> {methods.map((m) => ( <option key={m.method} value={m.method} disabled={!m.available} title={!m.available && m.requires?.length ? `Requires: ${m.requires.join(', ')}` : undefined}> {m.method}{!m.available ? ' (unavailable)' : ''} </option> ))} </select> <button type="button" className="btn" onClick={() => refetchMethods()}> Refresh </button> </div> </label> <label className="flex flex-col"> <span className="label">Horizon</span> <input className="input w-24" type="number" min={1} value={horizon} onChange={(e) => setHorizon(Number(e.target.value))} /> </label> <label className="flex flex-col"> <span className="label">Proxy</span> <select className="select w-40" value={proxy} onChange={(e) => setProxy(e.target.value)}> <option value="squared_return">squared_return</option> <option value="abs_return">abs_return</option> <option value="log_r2">log_r2</option> </select> </label> <button className="btn ml-auto" disabled={!canRun} onClick={run}> Run Vol Forecast </button> </div> {!selectedMeta?.available && ( <div className="text-xs text-amber-400"> {selectedMeta?.method} is not available.{' '} {selectedMeta?.requires?.length ? `Requires: ${selectedMeta.requires.join(', ')}` : 'Install the optional dependency.'} </div> )} {selectedMeta?.description && ( <div className="text-xs text-slate-400 max-w-3xl">{selectedMeta.description}</div> )} {selectedMeta && ( <details className="border border-slate-800 rounded-md" open={selectedMeta.params?.length ? advancedSummary !== 'Defaults' : false}> <summary className="cursor-pointer select-none px-3 py-2 text-sm font-medium text-slate-200 hover:bg-slate-800 flex justify-between items-center"> <span>Method Parameters</span> <span className="text-xs text-slate-400">{selectedMeta.params?.length ? advancedSummary : 'None'}</span> </summary> {selectedMeta.params?.length ? ( <div className="p-3 grid gap-3 sm:grid-cols-2"> {selectedMeta.params.map((param) => { const lower = param.type?.toLowerCase() ?? '' const value = activeParams[param.name] if (lower === 'bool') { const boolVal = typeof value === 'boolean' ? value : value === 'true' return ( <label key={param.name} className="flex flex-col gap-1" title={param.description || ''}> <span className="label">{param.name}</span> <select className="select" value={boolVal ? 'true' : 'false'} onChange={(e) => toggleBoolean(param.name, e.target.value === 'true')} > <option value="true">true</option> <option value="false">false</option> </select> {param.description ? <span className="text-xs text-slate-400">{param.description}</span> : null} </label> ) } return ( <label key={param.name} className="flex flex-col gap-1" title={param.description || ''}> <span className="label">{param.name}{param.type ? <em className="not-italic text-slate-500"> ({param.type})</em> : null}</span> <input className="input" value={stringifyValue(value, param.type)} onChange={(e) => updateParam(param.name, e.target.value, param)} placeholder={param.default !== undefined && param.default !== null ? stringifyValue(param.default, param.type) : ''} /> {param.description ? <span className="text-xs text-slate-400">{param.description}</span> : null} </label> ) })} </div> ) : ( <div className="p-3 text-xs text-slate-500">Selected method has no configurable parameters.</div> )} </details> )} </div> ) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/emerzon/mt-data-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server