{
  "name": "Regime Model (sanitized template)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9,
              "triggerAtMinute": 15
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        3328,
        4288
      ],
      "id": "aef98cf1-c3c2-49dc-af0d-df0132be94ab",
      "name": "Schedule Trigger"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "regime-model",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        3312,
        3984
      ],
      "id": "a95cb373-2e2e-4ac3-a2c6-70edca5c7eaa",
      "name": "Webhook"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "fb91d733-aa5e-4374-b717-a5239b402d99",
              "leftValue": "={{ $json.is_working_day }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        3984,
        4288
      ],
      "id": "4ee75aa5-a47b-49b3-97cb-1f6dd7abc2a1",
      "name": "If"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "wv-if2-cond-01",
              "leftValue": "={{ $json.route }}",
              "rightValue": "execute",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        3968,
        3984
      ],
      "id": "28dbed1d-ffea-4ef4-9457-b7f6348366eb",
      "name": "Webhook Validation"
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "=={{ JSON.stringify($json) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        4448,
        3808
      ],
      "id": "03c820e7-abeb-45d1-a5ee-f067596635da",
      "name": "Respond Error",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        4368,
        3952
      ],
      "id": "6d55709c-b185-434f-b42d-45aa96ec0acd",
      "name": "Merge Triggers"
    },
    {
      "parameters": {
        "jsCode": "const j = $input.first().json;\nreturn [{ json: {\n  tickers:        j.tickers      || ['SPY'],\n  start_date:     j.start_date   || '2019-01-01',\n  end_date:       j.end_date     || null,\n  jump_penalty:   j.jump_penalty != null ? j.jump_penalty : 5.0,\n  n_components:   j.n_components != null ? j.n_components : 2,\n  n_init:         j.n_init       != null ? j.n_init       : 20,\n  trigger_source: j.route === 'execute' ? 'webhook' : 'schedule',\n  has_csv:        j.has_csv      || false,\n  csv_data:       j.csv_data     || null\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4512,
        3952
      ],
      "id": "5ceff252-cf1a-4c5e-98f8-a893d46afa84",
      "name": "Set Parameters"
    },
    {
      "parameters": {
        "endpointUrl": "<mcp-endpoint-url>",
        "tool": {
          "__rl": true,
          "value": "fetch-ohlcvs",
          "mode": "list",
          "cachedResultName": "fetch-ohlcvs"
        },
        "parameters": {
          "mappingMode": "defineBelow",
          "value": {
            "request": "={{ {\n  \"tickers\": $('Set Parameters').first().json.tickers,\n  \"timeframe\": \"1d\",\n  \"start_date\": $('Set Parameters').first().json.start_date,\n  \"end_date\": $('Set Parameters').first().json.end_date || $now.toFormat('yyyy-MM-dd'),\n  \"_skill_contract_acknowledged\": \"read-and-understood\"\n} }}"
          },
          "matchingColumns": [
            "request"
          ],
          "schema": [
            {
              "id": "request",
              "displayName": "request",
              "defaultMatch": false,
              "required": true,
              "display": true,
              "type": "object",
              "defaultValue": "{\n  \"tickers\": [\n    \"string\"\n  ],\n  \"timeframe\": \"string\",\n  \"start_date\": \"string\",\n  \"end_date\": \"string\",\n  \"source\": \"string\",\n  \"interval\": \"string\"\n}",
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "typeVersion": 1,
      "position": [
        4464,
        4272
      ],
      "id": "1c998b33-bf74-4b09-8f0e-56600c31cee7",
      "name": "Data extraction"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "<mcp-endpoint-url>",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Accept",
              "value": "application/json, text/event-stream"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"method\": \"initialize\",\n  \"params\": {\n    \"protocolVersion\": \"2024-11-05\",\n    \"capabilities\": {},\n    \"clientInfo\": { \"name\": \"n8n\", \"version\": \"1.0.0\" }\n  }\n}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4688,
        4272
      ],
      "id": "19005181-1a98-4614-b100-037da08e38b2",
      "name": "DataUnpack"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "<mcp-endpoint-url>",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Accept",
              "value": "application/json, text/event-stream"
            },
            {
              "name": "mcp-session-id",
              "value": "={{ $('DataUnpack').first().json.headers['mcp-session-id'] }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ \"jsonrpc\": \"2.0\", \"method\": \"notifications/initialized\" }) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        5168,
        4272
      ],
      "id": "33ca40d9-7fda-42e3-b596-ac052795f81a",
      "name": "Connection Handshake"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "<mcp-endpoint-url>",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Accept",
              "value": "application/json, text/event-stream"
            },
            {
              "name": "mcp-session-id",
              "value": "={{ $('DataUnpack').first().json.headers['mcp-session-id'] }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\", \"params\": { \"name\": \"result_get\", \"arguments\": { \"request\": { \"result_id\": $('Data extraction').first().json.result_id } } } }) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4080,
        4528
      ],
      "id": "500fb5d4-3216-4be0-bd78-09d7f749aad3",
      "name": "DataExtracted"
    },
    {
      "parameters": {
        "jsCode": "// --- Determine data source: CSV or SSE from data extraction ---\nconst inputJson = $input.first().json;\nlet allDates, allCloses;\n\nif (inputJson.csv_rows) {\n  // === CSV PATH ===\n  const sorted = [...inputJson.csv_rows]\n    .sort((a, b) => a.timestamp.localeCompare(b.timestamp))\n    .filter(r => { const v = parseFloat(r.close); return !isNaN(v) && v > 0; });\n  allDates  = sorted.map(r => r.timestamp.substring(0, 10));\n  allCloses = sorted.map(r => parseFloat(r.close));\n} else {\n  // === DATA EXTRACTION PATH (SSE) ===\n  const raw = inputJson.data;\n  const dataIdx = raw.indexOf('data: {');\n  if (dataIdx === -1) throw new Error('No \"data: {\" found in response. Raw: ' + raw.substring(0, 200));\n  const jsonStr = raw.slice(dataIdx + 6).trim();\n  const outer   = JSON.parse(jsonStr);\n  const text    = outer?.result?.content?.[0]?.text;\n  const rows    = JSON.parse(text)?.result?.payload?.ohlcv_df?.rows;\n  if (!rows) throw new Error('No rows. text keys: ' + JSON.stringify(Object.keys(JSON.parse(text)?.result ?? {})));\n  const sorted = [...rows]\n    .sort((a, b) => a.timestamp.localeCompare(b.timestamp))\n    .filter(r => { const v = parseFloat(r.close); return r.close != null && !isNaN(v) && v > 0; });\n  allDates  = sorted.map(r => r.timestamp.substring(0, 10));\n  allCloses = sorted.map(r => parseFloat(r.close));\n}\nconst n         = allCloses.length;\n\nconst lr = [0.0];\nfor (let i = 1; i < n; i++) lr.push(Math.log(allCloses[i] / allCloses[i-1]));\n\nfunction rollingVol(rets, w) {\n  const out = new Array(rets.length).fill(0);\n  for (let i = w-1; i < rets.length; i++) {\n    const chunk = rets.slice(i-w+1, i+1);\n    const mu = chunk.reduce((a,b)=>a+b,0)/w;\n    const v  = chunk.reduce((a,b)=>a+(b-mu)**2,0)/Math.max(w-1,1);\n    out[i] = Math.sqrt(v*252);\n  }\n  return out;\n}\n\nfunction rollingSum(rets, w) {\n  const out = new Array(rets.length).fill(0);\n  for (let i = w-1; i < rets.length; i++)\n    out[i] = rets.slice(i-w+1, i+1).reduce((a,b)=>a+b,0);\n  return out;\n}\n\nfunction zscore(v) {\n  const mu  = v.reduce((a,b)=>a+b,0)/v.length;\n  const std = Math.sqrt(v.reduce((a,b)=>a+(b-mu)**2,0)/Math.max(v.length-1,1)) || 1e-8;\n  return v.map(x=>(x-mu)/std);\n}\n\nconst START     = 63;\nconst lr_f      = lr.slice(START);\nconst rv21_f    = rollingVol(lr, 21).slice(START);\nconst rv63_f    = rollingVol(lr, 63).slice(START);\nconst m10_f     = rollingSum(lr, 10).slice(START);\nconst featDates = allDates.slice(START);\nconst m         = lr_f.length;\nconst idx       = Array.from({length: m}, (_, i) => i);\n\nconst ret_z    = zscore(lr_f);\nconst rvol21_z = zscore(rv21_f);\nconst rvol63_z = zscore(rv63_f);\nconst mom10_z  = zscore(m10_f);\n\nconst hasNaN = idx.some(i => isNaN(ret_z[i]) || isNaN(rvol21_z[i]) || isNaN(rvol63_z[i]) || isNaN(mom10_z[i]));\nif (hasNaN) throw new Error('NaN detected in features after z-scoring');\n\nconst X = {\n  index:   idx,\n  columns: [\"ret_z\",\"rvol21_z\",\"rvol63_z\",\"mom10_z\"],\n  data:    idx.map(i => [ret_z[i], rvol21_z[i], rvol63_z[i], mom10_z[i]])\n};\n\nconst ret_ser = { index: idx, data: lr_f.map(v => parseFloat(v.toFixed(8))) };\n\nreturn [{ json: {\n  X, ret_ser,\n  feat_dates:  featDates,\n  all_dates:   allDates,\n  all_closes:  allCloses,\n  row_count:   m,\n  date_first:  featDates[0],\n  date_last:   featDates[m-1]\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4896,
        4272
      ],
      "id": "f4a65083-b338-487d-a23f-1b4a0ac06e06",
      "name": "Generate features"
    },
    {
      "parameters": {
        "endpointUrl": "<mcp-endpoint-url>",
        "tool": {
          "__rl": true,
          "value": "jump-model-create",
          "mode": "list",
          "cachedResultName": "jump-model-create"
        },
        "parameters": {
          "mappingMode": "defineBelow",
          "value": {
            "request": "={{ { \n  \"init_kwargs\": { \n    \"n_components\": $('Set Parameters').first().json.n_components, \n    \"jump_penalty\": $('Set Parameters').first().json.jump_penalty, \n    \"cont\": true, \n    \"n_init\": $('Set Parameters').first().json.n_init\n  }, \n  \"_skill_contract_acknowledged\": \"read-and-understood\" \n} }}"
          },
          "matchingColumns": [
            "request"
          ],
          "schema": [
            {
              "id": "request",
              "displayName": "request",
              "defaultMatch": false,
              "required": true,
              "display": true,
              "type": "string",
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "typeVersion": 1.1,
      "position": [
        4480,
        4576
      ],
      "id": "585fd72f-c21e-4c2e-831b-6a2c3f9e4b8e",
      "name": "Create Jump Model"
    },
    {
      "parameters": {
        "endpointUrl": "=<mcp-endpoint-url>",
        "tool": {
          "__rl": true,
          "value": "jump-model-fit",
          "mode": "list",
          "cachedResultName": "jump-model-fit"
        },
        "parameters": {
          "mappingMode": "defineBelow",
          "value": {
            "request": "={{ { \n  \"handle\": $('Create Jump Model').first().json.structuredContent.result.handle, \n  \"X\": $('Generate features').first().json.X, \n  \"ret_ser\": $('Generate features').first().json.ser, \n  \"sort_by\": \"cumret\",\n  \"_skill_contract_acknowledged\": \"read-and-understood\"\n} }}"
          },
          "matchingColumns": [
            "request"
          ],
          "schema": [
            {
              "id": "request",
              "displayName": "request",
              "defaultMatch": false,
              "required": true,
              "display": true,
              "type": "string",
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "typeVersion": 1.1,
      "position": [
        4704,
        4576
      ],
      "id": "f7de78fa-9bfb-49b3-a804-9348f9b550b8",
      "name": "Fit Model",
      "retryOnFail": false,
      "alwaysOutputData": false,
      "executeOnce": true,
      "notesInFlow": false
    },
    {
      "parameters": {
        "endpointUrl": "<mcp-endpoint-url>",
        "tool": {
          "__rl": true,
          "value": "jump-model-predict-proba-online",
          "mode": "list",
          "cachedResultName": "jump-model-predict-proba-online"
        },
        "parameters": {
          "mappingMode": "defineBelow",
          "value": {
            "request": "={{ { \"handle\": $('Create Jump Model').first().json.structuredContent.result.handle, \"X\": $('Generate features').first().json.X,\n\"_skill_contract_acknowledged\": \"read-and-understood\"\n} }}"
          },
          "matchingColumns": [
            "request"
          ],
          "schema": [
            {
              "id": "request",
              "displayName": "request",
              "defaultMatch": false,
              "required": true,
              "display": true,
              "type": "string",
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "typeVersion": 1.1,
      "position": [
        4896,
        4576
      ],
      "id": "66ff8005-3e72-4e18-9ee3-c7a2ba4a4a93",
      "name": "Predict Online Probabilities"
    },
    {
      "parameters": {
        "endpointUrl": "<mcp-endpoint-url>",
        "tool": {
          "__rl": true,
          "value": "jump-model-predict",
          "mode": "list",
          "cachedResultName": "jump-model-predict"
        },
        "parameters": {
          "mappingMode": "defineBelow",
          "value": {
            "request": "={{ { \"handle\": $('Create Jump Model').first().json.structuredContent.result.handle, \"X\": $('Generate features').first().json.X,\n\"_skill_contract_acknowledged\": \"read-and-understood\"} }}"
          },
          "matchingColumns": [
            "request"
          ],
          "schema": [
            {
              "id": "request",
              "displayName": "request",
              "defaultMatch": false,
              "required": true,
              "display": true,
              "type": "string",
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "typeVersion": 1.1,
      "position": [
        4080,
        4816
      ],
      "id": "6488352e-373a-48a6-9f1e-a78b94c57c3e",
      "name": "Predict Hard Labels"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "<mcp-endpoint-url>",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Accept",
              "value": "application/json, text/event-stream"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"n8n\",\"version\":\"1.0.0\"}}}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4272,
        4816
      ],
      "id": "0ca8e65e-4061-45db-a411-c825f335cc1a",
      "name": "Node A"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "<mcp-endpoint-url>",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Accept",
              "value": "application/json, text/event-stream"
            },
            {
              "name": "mcp-session-id",
              "value": "={{ $('Node A').first().json.headers['mcp-session-id'] }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}",
        "options": {
          "response": {
            "response": {}
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4416,
        4816
      ],
      "id": "c76c5d86-1211-49bc-bfe1-eee39c406879",
      "name": "Node B"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "<mcp-endpoint-url>",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Accept",
              "value": "application/json, text/event-stream"
            },
            {
              "name": "mcp-session-id",
              "value": "={{ $('Node A').first().json.headers['mcp-session-id'] }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ \n  \"jsonrpc\": \"2.0\", \n  \"id\": 3, \n  \"method\": \"tools/call\", \n  \"params\": { \n    \"name\": \"result_get\", \n    \"arguments\": { \n      \"request\": { \n        \"result_id\": $('Predict Online Probabilities').first().json.structuredContent.result_id,\n        \"_skill_contract_acknowledged\": \"read-and-understood\"\n      } \n    } \n  } \n}) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4560,
        4816
      ],
      "id": "83dad748-7727-4b67-bb1d-9af18b9e76eb",
      "name": "Node C"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "<mcp-endpoint-url>",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Accept",
              "value": "application/json, text/event-stream"
            },
            {
              "name": "mcp-session-id",
              "value": "={{ $('Node A').first().json.headers['mcp-session-id'] }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ \"jsonrpc\": \"2.0\", \"id\": 3, \"method\": \"tools/call\", \"params\": { \"name\": \"result_get\", \"arguments\": { \"request\": { \"result_id\": $('Predict Hard Labels').first().json.structuredContent.result_id,\"_skill_contract_acknowledged\": \"read-and-understood\" } } } }) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4704,
        4816
      ],
      "id": "ecc08cfd-95d1-469e-ad0a-6485492c0be2",
      "name": "Node D"
    },
    {
      "parameters": {
        "jsCode": "function parseSSE(raw) {\n  const idx = raw.indexOf('data: {');\n  if (idx === -1) throw new Error('No data found: ' + raw.slice(0, 100));\n  const outer = JSON.parse(raw.slice(idx + 6).trim());\n  const text  = outer?.result?.content?.[0]?.text;\n  return JSON.parse(text);\n}\n\nconst rawProba  = $('Node C').first().json.data;\nconst rawLabels = $('Node D').first().json.data;\n\nconst probaPayload  = parseSSE(rawProba);\nconst labelsPayload = parseSSE(rawLabels);\n\nconst probaRows  = probaPayload?.result?.payload?.rows  ?? probaPayload?.payload?.rows;\nconst labelRows  = labelsPayload?.result?.payload?.rows ?? labelsPayload?.payload?.rows;\n\nif (!probaRows)  throw new Error('No proba rows.  Keys: ' + JSON.stringify(Object.keys(probaPayload)));\nif (!labelRows)  throw new Error('No label rows. Keys: ' + JSON.stringify(Object.keys(labelsPayload)));\n\nprobaRows.sort((a, b)  => parseInt(a.index) - parseInt(b.index));\nlabelRows.sort((a, b)  => parseInt(a.index) - parseInt(b.index));\n\nconst labels = labelRows.map(r => parseInt(r.value));\nconst probas = probaRows.map(r => [parseFloat(r['0']), parseFloat(r['1'])]);\n\nconst featDates = $('Generate features').first().json.feat_dates;\nconst retData   = $('Generate features').first().json.ret_ser.data;\n\nconst sum0 = retData.reduce((a, r, i) => a + (labels[i] === 0 ? r : 0), 0);\nconst sum1 = retData.reduce((a, r, i) => a + (labels[i] === 1 ? r : 0), 0);\nconst bullComp = sum0 >= sum1 ? 0 : 1;\nconst bearComp = 1 - bullComp;\nconst stateMap = { [bullComp]: 'Bull', [bearComp]: 'Bear' };\n\nconst m       = labels.length;\nconst nBull   = labels.filter(l => l === bullComp).length;\nconst nBear   = m - nBull;\nconst nTrans  = labels.filter((l, i) => i > 0 && l !== labels[i-1]).length;\nconst curState = stateMap[labels[m-1]];\n\nlet streak = 1;\nfor (let i = m-2; i >= 0 && labels[i] === labels[m-1]; i--) streak++;\n\nconst pBull = (probas[m-1][bullComp] * 100).toFixed(1);\nconst pBear = (probas[m-1][bearComp] * 100).toFixed(1);\n\nconst last10 = Array.from({length: 10}, (_, i) => {\n  const idx = m - 10 + i;\n  return `  ${featDates[idx]}   ${stateMap[labels[idx]].padEnd(5)}   ${(probas[idx][bullComp]*100).toFixed(1).padStart(6)}%   ${(probas[idx][bearComp]*100).toFixed(1).padStart(6)}%`;\n}).join('\\n');\n\nconst tickers = $('Set Parameters').first().json.tickers;\nconst tickerStr = Array.isArray(tickers) ? tickers.join('/') : String(tickers);\nconst jumpPenalty = $('Set Parameters').first().json.jump_penalty;\n\nconst report = `\n=================================================================\n  JUMP MODEL  —  REGIME FORECAST         ${tickerStr}  |  λ=${jumpPenalty}\n=================================================================\n  Analysis date  : ${new Date().toISOString().split('T')[0]}\n  Sample         : ${featDates[0]} → ${featDates[m-1]}  (${m} days)\n  State mapping  : comp ${bullComp}=Bull  comp ${bearComp}=Bear\n  Regime stats   : Bull ${(100*nBull/m).toFixed(1)}% (${nBull}d)  Bear ${(100*nBear/m).toFixed(1)}% (${nBear}d)  Transitions ${nTrans}\n\n  Date           State    P(Bull)   P(Bear)\n  ----------     -----   --------  --------\n${last10}\n\n  Current regime    : ${curState}  (${streak} consecutive days)\n  Next-day forecast : P(Bull) = ${pBull}%   P(Bear) = ${pBear}%\n=================================================================`;\n\nreturn [{ json: {\n  report,\n  current_state:  curState,\n  p_bull:         parseFloat(pBull),\n  p_bear:         parseFloat(pBear),\n  streak_days:    streak,\n  bull_pct:       parseFloat((100*nBull/m).toFixed(1)),\n  bear_pct:       parseFloat((100*nBear/m).toFixed(1)),\n  n_transitions:  nTrans,\n  analysis_date:  new Date().toISOString().split('T')[0]\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4864,
        4816
      ],
      "id": "6b02516c-dc70-4705-a154-ee69d2518a8b",
      "name": "TextReport"
    },
    {
      "parameters": {
        "jsCode": "const featDates = $('Generate features').first().json.feat_dates;\nconst allDates  = $('Generate features').first().json.all_dates;\nconst allCloses = $('Generate features').first().json.all_closes;\nconst retData   = $('Generate features').first().json.ret_ser.data;\n\nfunction parseSSE(raw) {\n  const idx = raw.indexOf('data: {');\n  const outer = JSON.parse(raw.slice(idx + 6).trim());\n  return JSON.parse(outer?.result?.content?.[0]?.text);\n}\nconst probaRows = parseSSE($('Node C').first().json.data)?.result?.payload?.rows;\nconst labelRows = parseSSE($('Node D').first().json.data)?.result?.payload?.rows;\n\nprobaRows.sort((a, b) => parseInt(a.index) - parseInt(b.index));\nlabelRows.sort((a, b) => parseInt(a.index) - parseInt(b.index));\n\nconst labels = labelRows.map(r => parseInt(r.value));\nconst probas = probaRows.map(r => [parseFloat(r['0']), parseFloat(r['1'])]);\n\nconst sum0    = retData.reduce((a,r,i) => a + (labels[i]===0 ? r : 0), 0);\nconst bullComp = sum0 >= retData.reduce((a,r,i) => a+(labels[i]===1?r:0),0) ? 0 : 1;\nconst bearComp = 1 - bullComp;\n\nconst startIdx = allDates.indexOf(featDates[0]);\n\nconst bands = [];\nlet bandStart = 0;\nfor (let i = 1; i <= labels.length; i++) {\n  if (i === labels.length || labels[i] !== labels[i-1]) {\n    bands.push({ from: featDates[bandStart], to: featDates[Math.min(i, labels.length-1)], bull: labels[bandStart] === bullComp });\n    bandStart = i;\n  }\n}\n\nconst pBullSeries = probas.map(p => parseFloat((p[bullComp]*100).toFixed(2)));\n\nconst bandsJson  = JSON.stringify(bands);\nconst datesJson  = JSON.stringify(allDates);\nconst closesJson = JSON.stringify(allCloses);\nconst featJson   = JSON.stringify(featDates);\nconst pBullJson  = JSON.stringify(pBullSeries);\nconst today      = new Date().toISOString().split('T')[0];\n\nconst tickers = $('Set Parameters').first().json.tickers;\nconst tickerStr = Array.isArray(tickers) ? tickers.join('/') : String(tickers);\nconst jumpPenalty = $('Set Parameters').first().json.jump_penalty;\n\nconst html = `<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <title>${tickerStr} Jump Model Regimes</title>\n  <script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js\"></script>\n  <script src=\"https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1/dist/chartjs-plugin-annotation.min.js\"></script>\n  <style>\n    body { background:#0f0f1a; color:#ccc; font-family:Arial,sans-serif; margin:0; padding:20px; }\n    h2   { color:#fff; text-align:center; margin-bottom:4px; }\n    p    { text-align:center; color:#888; font-size:13px; margin-bottom:20px; }\n    .chart-wrap { background:#161625; border-radius:10px; padding:16px; margin-bottom:20px; }\n  </style>\n</head>\n<body>\n  <h2>${tickerStr} — Jump Model 2-State Regime  |  λ = ${jumpPenalty}</h2>\n  <p>Analysis date: ${today} &nbsp;|&nbsp; Sample: ${featDates[0]} → ${featDates[featDates.length-1]}</p>\n  <div class=\"chart-wrap\"><canvas id=\"priceChart\" height=\"90\"></canvas></div>\n  <div class=\"chart-wrap\"><canvas id=\"probaChart\" height=\"40\"></canvas></div>\n<script>\nconst allDates  = ${datesJson};\nconst allCloses = ${closesJson};\nconst featDates = ${featJson};\nconst pBull     = ${pBullJson};\nconst bands     = ${bandsJson};\nconst regimeBg = {\n  id: 'regimeBg',\n  beforeDatasetsDraw(chart) {\n    const { ctx, scales: { x, y } } = chart;\n    if (!x || !y) return;\n    bands.forEach(b => {\n      const x0 = x.getPixelForValue(b.from);\n      const x1 = x.getPixelForValue(b.to);\n      ctx.fillStyle = b.bull ? 'rgba(39,174,96,0.18)' : 'rgba(231,76,60,0.18)';\n      ctx.fillRect(x0, y.top, x1 - x0, y.bottom - y.top);\n    });\n  }\n};\nnew Chart(document.getElementById('priceChart'), {\n  type: 'line', plugins: [regimeBg],\n  data: { labels: allDates, datasets: [{ label: tickerStr + ' Close', data: allCloses, borderColor: '#00d4ff', borderWidth: 1.2, pointRadius: 0, tension: 0.1 }] },\n  options: { responsive: true, animation: false, plugins: { legend: { labels: { color:'#aaa' } }, tooltip: { mode:'index', intersect:false } }, scales: { x: { ticks: { color:'#666', maxTicksLimit:10, maxRotation:0 }, grid: { color:'#1e1e2e' } }, y: { ticks: { color:'#aaa' }, grid: { color:'#1e1e2e' }, title: { display:true, text:'Price (USD)', color:'#aaa' } } } }\n});\nnew Chart(document.getElementById('probaChart'), {\n  type: 'line',\n  data: { labels: featDates, datasets: [{ label: 'P(Bull)', data: pBull, borderColor: '#27ae60', backgroundColor: (ctx) => { const g = ctx.chart.ctx.createLinearGradient(0,0,0,200); g.addColorStop(0,'rgba(39,174,96,0.4)'); g.addColorStop(1,'rgba(39,174,96,0.0)'); return g; }, fill: true, borderWidth: 1.2, pointRadius: 0, tension: 0.2 }] },\n  options: { responsive: true, animation: false, plugins: { legend: { labels: { color:'#aaa' } } }, scales: { x: { ticks: { color:'#666', maxTicksLimit:10, maxRotation:0 }, grid: { color:'#1e1e2e' } }, y: { min:0, max:100, ticks: { color:'#aaa', callback: v => v+'%' }, grid: { color:'#1e1e2e' }, title: { display:true, text:'P(Bull)', color:'#aaa' } } } }\n});\n</script>\n</body>\n</html>`;\n\nreturn [{ json: { html, filename: tickerStr.toLowerCase().replace(/[^a-z0-9]/g,'_') + '_regime_' + today + '.html' } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5024,
        4816
      ],
      "id": "06327778-5c12-4750-b350-bd04fab37c45",
      "name": "GraphReport"
    },
    {
      "parameters": {
        "jsCode": "const r          = $('TextReport').first().json;\nconst featDates  = $('Generate features').first().json.feat_dates;\nconst retData    = $('Generate features').first().json.ret_ser.data;\n\nfunction parseSSE(raw) {\n  const idx = raw.indexOf('data: {');\n  const outer = JSON.parse(raw.slice(idx + 6).trim());\n  return JSON.parse(outer?.result?.content?.[0]?.text);\n}\nconst probaRows = parseSSE($('Node C').first().json.data)?.result?.payload?.rows;\nconst labelRows = parseSSE($('Node D').first().json.data)?.result?.payload?.rows;\n\nprobaRows.sort((a,b) => parseInt(a.index)-parseInt(b.index));\nlabelRows.sort((a,b) => parseInt(a.index)-parseInt(b.index));\n\nconst labels   = labelRows.map(r => parseInt(r.value));\nconst probas   = probaRows.map(r => [parseFloat(r['0']), parseFloat(r['1'])]);\nconst sum0     = retData.reduce((a,v,i) => a+(labels[i]===0?v:0), 0);\nconst bullComp = sum0 >= retData.reduce((a,v,i) => a+(labels[i]===1?v:0), 0) ? 0 : 1;\nconst bearComp = 1 - bullComp;\nconst stateMap = { [bullComp]:'Bull', [bearComp]:'Bear' };\nconst m        = labels.length;\n\nconst monthMap = {};\nfeatDates.forEach((d, i) => {\n  const mo = d.substring(0, 7);\n  if (!monthMap[mo]) monthMap[mo] = { bull:0, bear:0 };\n  labels[i] === bullComp ? monthMap[mo].bull++ : monthMap[mo].bear++;\n});\n\nconst monthCells = Object.entries(monthMap).map(([mo, v]) => {\n  const pct   = v.bull / (v.bull + v.bear);\n  const label = mo.substring(2);\n  let bg, text;\n  if (pct >= 0.7)       { bg='#1a6e3c'; text='#7fffb0'; }\n  else if (pct >= 0.5)  { bg='#2d5a3d'; text='#a8e6c3'; }\n  else if (pct >= 0.3)  { bg='#5a2d2d'; text='#e6a8a8'; }\n  else                  { bg='#6e1a1a'; text='#ffb0b0'; }\n  return `<td style=\"background:${bg};color:${text};font-size:9px;padding:4px 3px;text-align:center;border:1px solid #111;white-space:nowrap\">${label}</td>`;\n}).join('');\n\nconst last10Rows = Array.from({length:10}, (_,i) => {\n  const idx    = m - 10 + i;\n  const state  = stateMap[labels[idx]];\n  const pb     = (probas[idx][bullComp]*100).toFixed(1);\n  const pbr    = (probas[idx][bearComp]*100).toFixed(1);\n  const isLast = i === 9;\n  const rowBg  = isLast ? (state==='Bull'?'#1a3a1a':'#3a1a1a') : (i%2===0?'#1a1a2e':'#14142a');\n  const stateColor = state==='Bull' ? '#27ae60' : '#e74c3c';\n  return `<tr style=\"background:${rowBg}\"><td style=\"padding:6px 12px;font-size:12px;color:#aaa\">${featDates[idx]}</td><td style=\"padding:6px 12px;font-size:12px;color:${stateColor};font-weight:bold\">${isLast?'▶ ':''}${state}</td><td style=\"padding:6px 12px;font-size:12px;color:#27ae60;text-align:right\">${pb}%</td><td style=\"padding:6px 12px;font-size:12px;color:#e74c3c;text-align:right\">${pbr}%</td></tr>`;\n}).join('');\n\nconst tickers = $('Set Parameters').first().json.tickers;\nconst tickerStr = Array.isArray(tickers) ? tickers.join('/') : String(tickers);\nconst jumpPenalty = $('Set Parameters').first().json.jump_penalty;\n\nconst bannerBg    = r.current_state==='Bull' ? '#0d3320' : '#3a0d0d';\nconst bannerBorder= r.current_state==='Bull' ? '#27ae60' : '#e74c3c';\nconst bannerColor = r.current_state==='Bull' ? '#27ae60' : '#e74c3c';\nconst emoji       = r.current_state==='Bull' ? '🟢' : '🔴';\n\nconst html = `<html><body style=\"background:#0f0f1a;font-family:Arial,sans-serif;margin:0;padding:0\"><div style=\"max-width:700px;margin:0 auto;padding:24px\"><div style=\"text-align:center;margin-bottom:20px\"><h1 style=\"color:#fff;margin:0;font-size:20px\">${tickerStr} Jump Model — Regime Forecast</h1><p style=\"color:#666;margin:6px 0;font-size:13px\">${r.analysis_date} &nbsp;|&nbsp; λ=${jumpPenalty} &nbsp;|&nbsp; Sample: ${featDates[0]} → ${featDates[m-1]} &nbsp;(${m} days)</p></div><div style=\"background:${bannerBg};border:2px solid ${bannerBorder};border-radius:10px;padding:20px;text-align:center;margin-bottom:20px\"><div style=\"font-size:32px;margin-bottom:8px\">${emoji}</div><div style=\"color:${bannerColor};font-size:24px;font-weight:bold\">${r.current_state.toUpperCase()} REGIME</div><div style=\"color:#aaa;margin-top:8px;font-size:14px\">${r.streak_days} consecutive trading days &nbsp;|&nbsp; P(Bull) = <strong style=\"color:#27ae60\">${r.p_bull}%</strong> &nbsp; P(Bear) = <strong style=\"color:#e74c3c\">${r.p_bear}%</strong></div></div><table style=\"width:100%;border-collapse:collapse;margin-bottom:20px\"><tr>${[['Bull Days',r.bull_pct+'%  ('+Math.round(m*r.bull_pct/100)+'d)','#27ae60'],['Bear Days',r.bear_pct+'%  ('+Math.round(m*r.bear_pct/100)+'d)','#e74c3c'],['Transitions',r.n_transitions,'#f39c12'],['Streak',r.streak_days+' days',bannerColor]].map(([label,val,col])=>`<td style=\"background:#161625;border-radius:8px;padding:14px;text-align:center;border:1px solid #222;width:25%\"><div style=\"color:#666;font-size:11px;margin-bottom:4px\">${label}</div><div style=\"color:${col};font-size:18px;font-weight:bold\">${val}</div></td>`).join('<td style=\"width:8px\"></td>')}</tr></table><div style=\"background:#161625;border-radius:8px;padding:16px;margin-bottom:20px;border:1px solid #222\"><div style=\"color:#aaa;font-size:12px;margin-bottom:10px;font-weight:bold\">📅 MONTHLY REGIME TIMELINE <span style=\"margin-left:12px;color:#27ae60\">█ Bull</span><span style=\"margin-left:8px;color:#e74c3c\">█ Bear</span></div><div style=\"overflow-x:auto\"><table style=\"border-collapse:collapse;min-width:100%\"><tr>${monthCells}</tr></table></div></div><div style=\"background:#161625;border-radius:8px;padding:16px;margin-bottom:20px;border:1px solid #222\"><div style=\"color:#aaa;font-size:12px;margin-bottom:10px;font-weight:bold\">🕐 LAST 10 TRADING DAYS</div><table style=\"width:100%;border-collapse:collapse\"><thead><tr style=\"background:#0f0f1a\"><th style=\"padding:6px 12px;color:#666;font-size:11px;text-align:left\">Date</th><th style=\"padding:6px 12px;color:#666;font-size:11px;text-align:left\">State</th><th style=\"padding:6px 12px;color:#666;font-size:11px;text-align:right\">P(Bull)</th><th style=\"padding:6px 12px;color:#666;font-size:11px;text-align:right\">P(Bear)</th></tr></thead><tbody>${last10Rows}</tbody></table></div><div style=\"background:#161625;border-radius:8px;padding:16px;border:1px solid #222\"><div style=\"color:#aaa;font-size:12px;margin-bottom:10px;font-weight:bold\">📋 FULL REPORT</div><pre style=\"color:#7fc;font-size:11px;margin:0;white-space:pre-wrap;font-family:'Courier New',monospace;line-height:1.5\">${r.report}</pre></div><p style=\"color:#444;font-size:11px;text-align:center;margin-top:20px\">QuantAssistant &nbsp;|&nbsp; Jump Model 2-State Regime &nbsp;|&nbsp; ${tickerStr} &nbsp;|&nbsp; λ=${jumpPenalty}</p></div></body></html>`;\n\nreturn [{ json: {\n  html,\n  subject: `${emoji} ${tickerStr} ${r.current_state.toUpperCase()} — P(Bull)=${r.p_bull}% | ${r.streak_days}d streak | ${r.analysis_date}`\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5200,
        4816
      ],
      "id": "3796240e-5210-478d-b6b3-d06310d00364",
      "name": "BuildEmail"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "route-by-trigger-cond",
              "leftValue": "={{ $('Set Parameters').first().json.trigger_source }}",
              "rightValue": "webhook",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        5392,
        4816
      ],
      "id": "ecf31d69-dfdc-4666-84a7-8f2444e89b0e",
      "name": "Route by Trigger"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $('TextReport').first().json }}",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        5600,
        4528
      ],
      "id": "c5dcac33-bed2-4183-93f8-f3d01f3f96b6",
      "name": "Respond to Webhook Success"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "has-csv-cond-01",
              "leftValue": "={{ $json.has_csv }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        4672,
        3952
      ],
      "id": "dba67225-8bd1-458c-bebd-6815212a93bd",
      "name": "Has CSV?"
    },
    {
      "parameters": {
        "jsCode": "const setParams = $('Set Parameters').first().json;\nconst csvData   = setParams.csv_data;\n\nif (!csvData || !csvData.trim()) throw new Error('csv_data is empty');\n\nconst lines   = csvData.trim().split(/\\r?\\n/).map(l => l.trim()).filter(l => l);\nconst headers = lines[0].split(',').map(h => h.trim());\n\n// Find Date column (accepts: date, Date, DATE, timestamp, Timestamp)\nconst dateIdx = headers.findIndex(h => /^date$/i.test(h) || /^timestamp$/i.test(h));\nconst valIdx  = headers.findIndex((_, i) => i !== dateIdx);\n\nif (dateIdx === -1) throw new Error('No Date/Timestamp column found. Headers: ' + headers.join(', '));\nif (valIdx  === -1) throw new Error('CSV must have at least 2 columns (Date + value).');\n\nconst rows = [];\nfor (const line of lines.slice(1)) {\n  const cols = line.split(',').map(c => c.trim());\n  const ts   = (cols[dateIdx] || '').substring(0, 10);\n  const val  = parseFloat(cols[valIdx]);\n  if (ts.length === 10 && !isNaN(val) && val > 0) rows.push({ timestamp: ts, close: val });\n}\n\nif (rows.length < 70) throw new Error('Too few rows after parsing: ' + rows.length + '. Need at least 70 to compute features (63-bar warmup + data).');\n\nreturn [{ json: { csv_rows: rows, value_col: headers[valIdx], row_count: rows.length } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4992,
        3936
      ],
      "id": "e6372c7f-e095-4678-8752-cbc31feca248",
      "name": "Parse CSV"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        5584,
        3920
      ],
      "id": "cb4de546-7c25-4c34-af36-fbd2a3bd19a7",
      "name": "Merge Pre-Features"
    },
    {
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "import json\n\nitem = _items[0]\njson_data = item.get('json', item)\nbody = json_data.get('body', {}) or {}\n\nif isinstance(body, str):\n    try:\n        body = json.loads(body)\n    except Exception:\n        body = {}\n\ntickers = body.get('tickers', None)\nstart_date = body.get('start_date', None)\nend_date = body.get('end_date', None)\njump_penalty = body.get('jump_penalty', None)\nn_components = body.get('n_components', None)\nn_init = body.get('n_init', None)\n\ncsv_data = body.get('csv_data', None)\nif isinstance(csv_data, str) and not csv_data.strip():\n    csv_data = None\n\nif tickers is not None and not isinstance(tickers, list):\n    return [{'json': {'route': 'error', 'reason': 'tickers must be a list, e.g. [\"SPY\"]'}}]\n\nreturn [{'json': {\n    'route': 'execute',\n    'tickers': tickers or ['SPY'],\n    'start_date': start_date or '2019-01-01',\n    'end_date': end_date or None,\n    'jump_penalty': float(jump_penalty) if jump_penalty is not None else 5.0,\n    'n_components': int(n_components) if n_components is not None else 2,\n    'n_init': int(n_init) if n_init is not None else 20,\n    'csv_data': csv_data,\n    'has_csv': csv_data is not None\n}}]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3632,
        3984
      ],
      "id": "1d00e7a4-e02f-4af6-ac46-7bdb7959cafa",
      "name": "ExtractCSVbody"
    },
    {
      "parameters": {
        "language": "pythonNative",
        "pythonCode": "import pandas as pd\nfrom pandas.tseries.holiday import USFederalHolidayCalendar\n\ntoday = pd.Timestamp.today().normalize()\ncal = USFederalHolidayCalendar()\nholidays = cal.holidays(start=today.replace(month=1, day=1), end=today.replace(month=12, day=31))\nis_weekend = today.dayofweek >= 5\nis_holiday = today in holidays\nreturn {\"is_working_day\": not (is_weekend or is_holiday)}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3648,
        4288
      ],
      "id": "978302c9-c4d7-4a5f-a2ce-ae44700d7534",
      "name": "IsHoliday"
    },
    {
      "parameters": {
        "jsCode": "const reason = $input.first().json.reason || 'Invalid request';\n\nreturn [{ json: {\n  status: 'error',\n  reason: reason,\n  schema: {\n    description: 'POST a JSON body to run Jump Model regime analysis. All fields are optional.',\n    fields: {\n      tickers:      { type: 'array<string>',  default: ['SPY'],      example: ['QQQ'],           note: 'Must be an array, not a string.' },\n      start_date:   { type: 'YYYY-MM-DD',     default: '2019-01-01', example: '2020-01-01' },\n      end_date:     { type: 'YYYY-MM-DD',     default: 'today',      example: '2024-12-31' },\n      jump_penalty: { type: 'number',         default: 5.0,          example: 10,                note: 'Regime-switching cost. Higher = fewer transitions. Typical range: 1–100.' },\n      n_components: { type: 'integer',        default: 2,            example: 2,                 note: 'Number of regimes. Typically 2 (Bull/Bear).' },\n      n_init:       { type: 'integer',        default: 20,           example: 30,                note: 'Random restarts for fitting. Higher = more stable.' },\n      csv_data:     { type: 'string (CSV)',   default: null,         note: 'Optional. If provided, skips market-data fetch. Must have a Date column + one price column. Minimum 70 rows.', csv_format: 'Date,Close\\n2024-01-02,469.42\\n2024-01-03,468.69\\n...' }\n    },\n    validation_rules: [\n      'tickers must be an array — e.g. [\"SPY\"], not \"SPY\"',\n      'csv_data must contain at least 70 rows (63-bar warmup + data)',\n      'csv_data must have a column named Date or Timestamp (case-insensitive)'\n    ],\n    example_requests: {\n      minimal:       {},\n      custom_ticker: { tickers: ['QQQ'] },\n      date_range:    { tickers: ['SPY'], start_date: '2020-01-01', end_date: '2024-12-31' },\n      custom_model:  { tickers: ['SPY'], jump_penalty: 10, n_components: 2, n_init: 30 },\n      csv_upload:    { tickers: ['CUSTOM'], csv_data: 'Date,Close\\n2024-01-02,469.42\\n...' }\n    },\n    curl_example: \"curl -X POST https://n8n.limex.pro/webhook/regime-model -H 'Content-Type: application/json' -d '{\\\"tickers\\\":[\\\"QQQ\\\"],\\\"jump_penalty\\\":10}'\"\n  }\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4096,
        3808
      ],
      "id": "25715ec3-e79f-4447-b17d-5093cf20ad15",
      "name": "Build Error Response"
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "IsHoliday",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "ExtractCSVbody",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If": {
      "main": [
        [
          {
            "node": "Merge Triggers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Validation": {
      "main": [
        [
          {
            "node": "Merge Triggers",
            "type": "main",
            "index": 1
          }
        ],
        [
          {
            "node": "Build Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Triggers": {
      "main": [
        [
          {
            "node": "Set Parameters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Parameters": {
      "main": [
        [
          {
            "node": "Has CSV?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has CSV?": {
      "main": [
        [
          {
            "node": "Parse CSV",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Data extraction",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse CSV": {
      "main": [
        [
          {
            "node": "Merge Pre-Features",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Data extraction": {
      "main": [
        [
          {
            "node": "DataUnpack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DataUnpack": {
      "main": [
        [
          {
            "node": "Connection Handshake",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Connection Handshake": {
      "main": [
        [
          {
            "node": "DataExtracted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DataExtracted": {
      "main": [
        [
          {
            "node": "Merge Pre-Features",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Pre-Features": {
      "main": [
        [
          {
            "node": "Generate features",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate features": {
      "main": [
        [
          {
            "node": "Create Jump Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Jump Model": {
      "main": [
        [
          {
            "node": "Fit Model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fit Model": {
      "main": [
        [
          {
            "node": "Predict Online Probabilities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Predict Online Probabilities": {
      "main": [
        [
          {
            "node": "Predict Hard Labels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Predict Hard Labels": {
      "main": [
        [
          {
            "node": "Node A",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Node A": {
      "main": [
        [
          {
            "node": "Node B",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Node B": {
      "main": [
        [
          {
            "node": "Node C",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Node C": {
      "main": [
        [
          {
            "node": "Node D",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Node D": {
      "main": [
        [
          {
            "node": "TextReport",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "TextReport": {
      "main": [
        [
          {
            "node": "GraphReport",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GraphReport": {
      "main": [
        [
          {
            "node": "BuildEmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BuildEmail": {
      "main": [
        [
          {
            "node": "Route by Trigger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Trigger": {
      "main": [
        [
          {
            "node": "Respond to Webhook Success",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "ExtractCSVbody": {
      "main": [
        [
          {
            "node": "Webhook Validation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IsHoliday": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Error Response": {
      "main": [
        [
          {
            "node": "Respond Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "nodeGroups": []
}
