8
n8n 中文网amn8n.com

移动平均线交叉股票提醒机器人

中级

这是一个Crypto Trading领域的自动化工作流,包含 14 个节点。主要使用 If, Set, Code, Postgres, SplitOut 等节点。 使用 Alpha Vantage 和 Discord 的股市技术分析提醒

前置要求
  • PostgreSQL 数据库连接信息
  • 可能需要目标 API 的认证凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "name": "移动平均线交叉股票提醒机器人",
  "tags": [],
  "nodes": [
    {
      "id": "f3acef6a-a25d-4f8a-ba03-62731eb2caff",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -480,
        0
      ],
      "parameters": {
        "color": 4,
        "width": 700,
        "height": 200,
        "content": "## 📊 股票市场分析工作流"
      },
      "typeVersion": 1
    },
    {
      "id": "5860551a-7e28-4a2e-9b57-9c9eee052027",
      "name": "触发器 - 每日收盘",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -420,
        260
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 17
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "db84cf85-94dd-401e-996c-30961d12d496",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        220,
        460
      ],
      "parameters": {
        "width": 400,
        "height": 220,
        "content": "## 📈 数据收集阶段"
      },
      "typeVersion": 1
    },
    {
      "id": "694bb8c3-5c80-44db-831d-70dce4ba872b",
      "name": "计算60/120日SMA",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        260
      ],
      "parameters": {
        "jsCode": "try {\n  // 1. Configurable windows\n  const SMA_SHORT = 60;\n  const SMA_LONG  = 120;\n  const NEEDED    = SMA_LONG + 1;  // long window + one extra day\n  // 2. Pull in all rows (each item.json has { id, symbol, Date, Close })\n  const rows = items.map(i => i.json);\n  // 3. Group rows by symbol\n  const bySymbol = {};\n  for (const r of rows) {\n    if (!bySymbol[r.symbol]) bySymbol[r.symbol] = [];\n    bySymbol[r.symbol].push(r);\n  }\n  // 4. SMA helper\n  function sma(arr, n, offset = 0) {\n    const slice = arr.slice(offset, offset + n);\n    return slice.reduce((sum, v) => sum + v, 0) / n;\n  }\n  // 5. Compute per‐symbol\n  const output = [];\n  for (const symbol of Object.keys(bySymbol)) {\n    const group = bySymbol[symbol]\n      .sort((a, b) => new Date(b.Date) - new Date(a.Date))  // newest→oldest\n      .slice(0, NEEDED);\n    if (group.length < NEEDED) {\n      output.push({\n        json: {\n          symbol,\n          error: `Not enough data for ${symbol}: ${group.length}/${NEEDED} days`\n        }\n      });\n      continue;\n    }\n    const closes = group.map(r => parseFloat(r.Close));\n    output.push({\n      json: {\n        symbol,\n        sma60_current:   sma(closes, SMA_SHORT, 0),\n        sma120_current:  sma(closes, SMA_LONG,  0),\n        sma60_previous:  sma(closes, SMA_SHORT, 1),\n        sma120_previous: sma(closes, SMA_LONG,  1),\n      }\n    });\n  }\n  return output;\n} catch (err) {\n  // global error fallback\n  return [{ json: { error: err.message } }];\n}"
      },
      "retryOnFail": false,
      "typeVersion": 2,
      "alwaysOutputData": false
    },
    {
      "id": "f6aae56c-2129-4435-8895-89759829cdcf",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        940,
        460
      ],
      "parameters": {
        "color": 5,
        "width": 380,
        "height": 260,
        "content": "## 🧮 技术分析引擎"
      },
      "typeVersion": 1
    },
    {
      "id": "c9101e92-f8af-4ef6-b28e-18a1ccc932b5",
      "name": "拆分 - 股票代码",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        20,
        260
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "symbol"
      },
      "typeVersion": 1
    },
    {
      "id": "c31e3d37-1926-46bc-86a9-f89e3927704b",
      "name": "获取每日历史数据",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        240,
        260
      ],
      "parameters": {
        "url": "https://www.alphavantage.co/query",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "function",
              "value": "TIME_SERIES_DAILY"
            },
            {
              "name": "symbol",
              "value": "={{$json[\"symbol\"]}}"
            },
            {
              "name": "apikey",
              "value": "YOURKEYHERE"
            },
            {
              "name": "outputsize",
              "value": "compact"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "f9b56387-fef9-4efc-943b-ac0971f9d522",
      "name": "设置 - 股票代码列表",
      "type": "n8n-nodes-base.set",
      "notes": "* IWM: Russell 2000 small-cap ETF – captures breadth/volatility outside the large-cap space\n\n* INTC: Intel – value-oriented semiconductor play that often lags/drifts differently than NVDA\n\n* JPM: JP Morgan – bellwether for the financial sector (banks & credit), interest-rate sensitivity\n\n* META: Meta Platforms – mega-cap digital advertising/social media momentum outside pure semis\n\n* NVDA: NVIDIA – leading-edge GPU/AI growth driver, often the pace-setter in tech rallies\n\n* PG: Procter & Gamble – defensive consumer staples, counter-cyclical when risk assets wobble\n\n* SPY: S\\&P 500 ETF – broad large-cap benchmark, anchors the overall market trend\n\n* TSLA: Tesla – high-beta auto/EV hybrid, adds extra swing-intensity vs. other tech names\n\n* XLB: Materials ETF – pure play on chemicals, metals & mining, driven by commodity cycles\n\n* XLE: Energy ETF – oil & gas sector, sensitive to crude swings, low correlation with semis\n\n* XLI: Industrials ETF – aerospace, transport & machinery, captures industrial-cycle turns\n\n* XLU: Utilities ETF – defensive “bond-like” sector, shines in risk-off/stress regimes\n\n* XLV: Health Care ETF – pharma/biotech/devices, often moves independently of the wider market",
      "position": [
        -200,
        260
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ec70e7cd-3ee8-45d9-bf2e-c53483296ec9",
              "name": "symbol",
              "type": "array",
              "value": "[\"NVDA\",\"JPM\",\"PG\",\"SPY\"]"
            }
          ]
        }
      },
      "notesInFlow": false,
      "typeVersion": 3.4
    },
    {
      "id": "2aec21da-cbba-4574-9794-98ccdafb268b",
      "name": "获取今日数据",
      "type": "n8n-nodes-base.code",
      "position": [
        460,
        260
      ],
      "parameters": {
        "jsCode": "return items.map(item => {\n  const payload = item.json;\n  const ts      = payload[\"Time Series (Daily)\"];\n  \n  // ← GUARD against missing or bad TS object\n  if (!ts || typeof ts !== 'object') {\n    return {\n      json: {\n        symbol: payload[\"Meta Data\"]?.[\"2. Symbol\"] || null,\n        error:  payload.Note \n             || payload[\"Error Message\"] \n             || \"No time series returned\",\n      }\n    };\n  }\n\n  const dates = Object.keys(ts).sort((a,b) => new Date(a) - new Date(b));\n  if (dates.length < 2) {\n    throw new Error(\"Not enough data to pick the day before last\");\n  }\n\n  const date = dates[dates.length - 2];\n  const day  = ts[date];\n\n  return {\n    json: {\n      symbol: payload[\"Meta Data\"][\"2. Symbol\"],\n      date,\n      close: day[\"4. close\"],\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "825fab86-cdea-49f3-b162-65b2d4e26ea0",
      "name": "在表中插入行",
      "type": "n8n-nodes-base.postgres",
      "position": [
        680,
        260
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "historical_stocks",
          "cachedResultName": "historical_stocks"
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "columns": {
          "value": {
            "Date": "={{ $json.date }}",
            "Close": "={{ $json.close }}",
            "symbol": "={{ $json.symbol }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "number",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "symbol",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "symbol",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date",
              "type": "dateTime",
              "display": true,
              "required": true,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Close",
              "type": "number",
              "display": true,
              "required": true,
              "displayName": "Close",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "skipOnConflict": true
        }
      },
      "retryOnFail": false,
      "typeVersion": 2.6,
      "alwaysOutputData": false
    },
    {
      "id": "6389ccdd-02e0-48b8-93e8-730cbdfda1b6",
      "name": "HTTP请求",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2200,
        260
      ],
      "parameters": {
        "url": "https://discord.com/api/webhooks/YOURWEBHOOKHERE",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendQuery": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "content",
              "value": "={{$json[\"content\"]}}"
            }
          ]
        },
        "queryParameters": {
          "parameters": [
            {}
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d0eb4106-9bb0-4752-bb68-59c9d5d15d1a",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2080,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 380,
        "height": 220,
        "content": "## 🔔 Discord通知"
      },
      "typeVersion": 1
    },
    {
      "id": "6420c403-0bf5-4de3-a82b-080292be75ec",
      "name": "如果(📈)",
      "type": "n8n-nodes-base.if",
      "position": [
        1340,
        260
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f2b85554-8cec-49f5-8bd4-cccc1e15cc6f",
              "operator": {
                "type": "number",
                "operation": "lte"
              },
              "leftValue": "={{ $json.sma60_previous }}",
              "rightValue": "={{ $json.sma120_previous }}"
            },
            {
              "id": "83d8772f-d217-4b76-84d1-b23bf245357c",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.sma60_current }}",
              "rightValue": "={{ $json.sma120_current }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1910f369-5404-45e6-a225-048b5fc34316",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1320,
        -120
      ],
      "parameters": {
        "color": 6,
        "width": 320,
        "height": 260,
        "content": "## 🚦 信号检测逻辑"
      },
      "typeVersion": 1
    },
    {
      "id": "1ef9e403-7021-45d5-91b6-cea5f7e30a3e",
      "name": "如果(📉)",
      "type": "n8n-nodes-base.if",
      "position": [
        1560,
        360
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f9e8087a-af4d-43e9-b0da-f12d9564b48f",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.sma60_previous }}",
              "rightValue": "={{ $json.sma120_previous }}"
            },
            {
              "id": "53c773b1-9740-43d7-b5cd-04e8b6256ace",
              "operator": {
                "type": "number",
                "operation": "lt"
              },
              "leftValue": "={{ $json.sma60_current }}",
              "rightValue": "={{ $json.sma120_current }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "77a4c20b-a7a1-46e2-8ffa-70b4310d75f9",
      "name": "设置 - 无信号消息",
      "type": "n8n-nodes-base.set",
      "position": [
        1780,
        260
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "content",
              "value": "={{ `🟡↔️ No crossover today for ${$items().map(i=>i.json.symbol).sort((a,b)=>a.localeCompare(b)).join(\", \")}. Monitoring continues…` }}"
            }
          ]
        },
        "options": {},
        "keepOnlySet": true
      },
      "executeOnce": true,
      "typeVersion": 2
    },
    {
      "id": "c7f0cba2-2d46-483a-9f8e-ca741da5c2e6",
      "name": "设置 - 死亡交叉消息",
      "type": "n8n-nodes-base.set",
      "position": [
        1780,
        460
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "content",
              "value": "={{ `🔴📉 Death Cross Alert for **${$json[\"symbol\"]}**! The 60-day SMA has crossed below the 120-day SMA.` }}"
            }
          ]
        },
        "options": {},
        "keepOnlySet": true
      },
      "typeVersion": 2
    },
    {
      "id": "3926135c-6308-479d-9e1f-1da7248a01ea",
      "name": "设置 - 黄金交叉消息",
      "type": "n8n-nodes-base.set",
      "position": [
        1780,
        60
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "content",
              "value": "={{ `🟢📈 Golden Cross Alert for **${$json[\"symbol\"]}**! The 60-day SMA has crossed above the 120-day SMA.` }}"
            }
          ]
        },
        "options": {},
        "keepOnlySet": true
      },
      "typeVersion": 2
    },
    {
      "id": "34474f1f-9caf-4d6e-bbd9-294aa8a73cfb",
      "name": "执行 SQL 查询",
      "type": "n8n-nodes-base.postgres",
      "position": [
        900,
        260
      ],
      "parameters": {
        "query": "WITH numbered AS (\n  SELECT\n    *,\n    ROW_NUMBER() OVER (\n      PARTITION BY symbol\n      ORDER BY \"Date\" DESC\n    ) AS rn\n  FROM public.historical_stocks\nWHERE symbol IN ('NVDA','JPM','SPY','PG')\n)\nSELECT\n  id,\n  symbol,\n  \"Date\",\n  \"Close\"\nFROM numbered\nWHERE rn <= 121\nORDER BY symbol, \"Date\" DESC;",
        "options": {},
        "operation": "executeQuery"
      },
      "executeOnce": true,
      "typeVersion": 2.6
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "If (📈)": {
      "main": [
        [
          {
            "node": "Set - Golden Cross Msg",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "If (📉)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If (📉)": {
      "main": [
        [
          {
            "node": "Set - Death Cross Msg",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set - No Signal Msg",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split - Tickers": {
      "main": [
        [
          {
            "node": "Fetch Daily History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set - Ticker List": {
      "main": [
        [
          {
            "node": "Split - Tickers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute 60/120 SMAs": {
      "main": [
        [
          {
            "node": "If (📈)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute a SQL query": {
      "main": [
        [
          {
            "node": "Compute 60/120 SMAs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Daily History": {
      "main": [
        [
          {
            "node": "Getting today's data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set - No Signal Msg": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Getting today's data": {
      "main": [
        [
          {
            "node": "Insert rows in a table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set - Death Cross Msg": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Trigger - Daily Close": {
      "main": [
        [
          {
            "node": "Set - Ticker List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert rows in a table": {
      "main": [
        [
          {
            "node": "Execute a SQL query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set - Golden Cross Msg": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

中级 - 加密货币交易

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
中级
节点数量14
分类1
节点类型7
难度说明

适合有一定经验的用户,包含 6-15 个节点的中等复杂度工作流

作者
Raz Hadas

Raz Hadas

@raz-hadas

Co-founder of buildmyflow, on a mission to create powerful and easy-to-use n8n automation templates. With a background in AI and a passion for social impact as the co-founder of TovTech, I'm dedicated to building a community-focused resource for free and premium workflows that save you time and unlock new possibilities. Let's automate together! https://www.linkedin.com/in/raz-hadas/

外部链接
在 n8n.io 查看

分享此工作流