<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nyria</title>
    <description>The latest articles on DEV Community by Nyria (@nyriaautomation).</description>
    <link>https://dev.to/nyriaautomation</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4196492%2Fd15af64b-39cb-4f28-8bd7-51bd8fbb1d50.png</url>
      <title>DEV Community: Nyria</title>
      <link>https://dev.to/nyriaautomation</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nyriaautomation"/>
    <language>en</language>
    <item>
      <title>Evaluate an options alert service before you pay</title>
      <dc:creator>Nyria</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:41:20 +0000</pubDate>
      <link>https://dev.to/nyriaautomation/evaluate-an-options-alert-service-before-you-pay-2b98</link>
      <guid>https://dev.to/nyriaautomation/evaluate-an-options-alert-service-before-you-pay-2b98</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nyria.io/guides/evaluate-options-alert-services" rel="noopener noreferrer"&gt;nyria.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every options alert service looks the same from the outside: a price, a screenshot, a channel you cannot see into. This guide is how to grade one from the inside, with its own posts, on a paper account that costs nothing, before a month's fee or a single live order. It ranks no services and names none.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a usable alert contains
&lt;/h2&gt;

&lt;p&gt;Five fields. An alert missing any of them is a trade idea, and a bot cannot place a trade idea.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ticker&lt;/strong&gt; The underlying, written as the exchange lists it: SPY, not "spiders". A call with no ticker is commentary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Side and action&lt;/strong&gt; Call or put, and whether the post opens or closes. BTO, STC and the rest, or plain words such as "out of SPY calls here". An exit that is implied, not posted, cannot be counted and cannot be traded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Strike&lt;/strong&gt; The exact strike, as listed. A strike that is not on the chain gets no contract, and a bot checks the chain rather than rounding for the caller.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expiry&lt;/strong&gt; Month and day. A call with no expiry takes the nearest listed expiration, which can be today's, even late in the day. If the caller leaves the date off, that is the trade they are posting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Price&lt;/strong&gt; The price the caller got. Without it there is nothing to compare a fill against, and the record is a list of tickers.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The notation does not matter; the fields do. Paste a week of the room's posts into the &lt;a href="https://nyria.io/tools/alert-tester" rel="noopener noreferrer"&gt;alert tester&lt;/a&gt; and see which ones read as an order and which get a refusal. Nothing is traded and no account is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example: one call, graded
&lt;/h2&gt;

&lt;p&gt;An entry and its exit as a room would post them, and the three numbers a paper bot gives you for each.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Entry, posted at 9:47&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BTO SPY 775c 11/20 @ 1.85
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Exit, posted at 10:12&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STC SPY 775c 11/20 @ 2.40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The paper bot reads the first as buy to open the SPY 775 call expiring Nov 20, sized from its own budget per entry, and the second as sell to close. Its Logs tab stamps the alert and the order, so for each call you hold three numbers side by side: the price the caller posted, the time the post arrived, and the price the paper bot filled at. Over a few weeks that table is the track record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grade it on paper, not on a screenshot
&lt;/h2&gt;

&lt;p&gt;Run the service on a free paper bot for a few weeks and let the caller's real posts fire. Two ways in. If the caller publishes the calls as a Nyria strategy, subscribe and deploy a bot on Nyria Paper. If you run the server or the Telegram group, link it under Integrations, create a strategy with that channel as the alert source, and add an author filter so only the caller's posts count.&lt;/p&gt;

&lt;p&gt;Read the paper numbers for what they are. Nyria Paper fills a market order at the quoted bid or ask, so it pays the spread but never slips, and a limit exit inside the spread fills at once at its own price. Paper P&amp;amp;L reads better than live. A service that cannot hold up on paper will not hold up live; one that does has passed a test, not every test.&lt;/p&gt;

&lt;p&gt;Count the exits. A position closes only when a post reads as an exit, so calls the caller never closed in writing stay open in the paper account, and they would stay open at your broker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fill timing: a person versus a bot
&lt;/h2&gt;

&lt;p&gt;The caller fills first, then types. A reader then sees the post, opens the broker and types again. That is two gaps, and a bot removes only the second one. Discord and Telegram stamp every message, so compare the caller's posted price with where the contract was trading at the stamp. A gap of seconds is normal. A gap of minutes, on a contract expiring today, is the whole edge.&lt;/p&gt;

&lt;p&gt;A bot does not wait for a better price either. On Telegram, an entry Nyria handles more than a minute after it was posted is not placed at all; the strategy log says so, and a close in the same message still runs. Whatever the source, the paper fill tells you what the post was worth by the time anything could act on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red flags
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A promise about returns&lt;/strong&gt; A percentage on a banner is a claim, not a record. The only record you can audit is the channel history: scroll it, count the entries, match each to its exit, and note the ones that never closed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No exits posted&lt;/strong&gt; A room that posts entries and leaves the exit to you is half a service, and the half you cannot grade. Partial exits and trims count if they are posted every time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Alerts that change after the fact&lt;/strong&gt; An edited or deleted post is a different record from the one that was traded. Nyria does not read an edit again, and editing or deleting a post after it trades does not undo the order, so what the bot logged is what was posted at the time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A track record that is a screenshot&lt;/strong&gt; A picture of a month's P&amp;amp;L shows one account on one day. It does not show which calls were skipped, which were closed late, or what a reader could have filled at.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Calls mixed into chat&lt;/strong&gt; Alerts in a channel of their own, from one author, in one notation, can be read at a glance and by a bot. A channel where calls, chat and screenshots share the stream is slower to read by eye and harder for anything else to read at all.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Going live later, with your own broker
&lt;/h2&gt;

&lt;p&gt;When the paper table holds up, deploy a bot on the live account, or edit the paper bot and pick the live account. The first live deploy asks you to accept the risk disclosure. Orders go to Charles Schwab, Tradier, tastytrade or Alpaca, in your own account, and the money never leaves your broker.&lt;/p&gt;

&lt;p&gt;Paper did not test slippage, partial fills, broker rejections or assignment, so start with a budget per entry you can afford to lose in full, and hold capital for both the entry and the exit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can Nyria read a paid Discord I only belong to?
&lt;/h3&gt;

&lt;p&gt;No. Nyria reads channels in servers you run, and groups or channels you administer on Telegram. If the caller publishes the calls as a Nyria strategy, you subscribe to it and deploy a bot in your own account.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long should the paper test run?
&lt;/h3&gt;

&lt;p&gt;Long enough to see the service in more than one kind of week. The docs ask for one to two weeks of paper before a first live bot; for grading a service you are about to pay for, a few weeks is a fairer sample.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the test cost?
&lt;/h3&gt;

&lt;p&gt;Nothing. Paper is free. Live trading from $15 a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grade an alert service on a paper bot
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://app.nyria.io/login?mode=signup" rel="noopener noreferrer"&gt;Start a paper bot free&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>trading</category>
      <category>finance</category>
      <category>options</category>
      <category>beginners</category>
    </item>
    <item>
      <title>No-code trading bot: stock and option alerts in, orders out</title>
      <dc:creator>Nyria</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:18:15 +0000</pubDate>
      <link>https://dev.to/nyriaautomation/no-code-trading-bot-stock-and-option-alerts-in-orders-out-54ed</link>
      <guid>https://dev.to/nyriaautomation/no-code-trading-bot-stock-and-option-alerts-in-orders-out-54ed</guid>
      <description>&lt;p&gt;I write code for a living, and the trading setup I actually run has no code in it. The signal lives in a TradingView chart. The alert is one line of text. The thing on the other end turns that line into an order at my broker. I used to run a VPS with a Flask app in the middle, and I don't miss it.&lt;/p&gt;

&lt;p&gt;Disclosure: I build Nyria, which is the thing on the other end here. Everything below is how it actually behaves, including the parts that will annoy you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the alert comes from
&lt;/h2&gt;

&lt;p&gt;Three sources, one destination.&lt;/p&gt;

&lt;p&gt;TradingView: an alert on an indicator or a strategy, with the strategy's webhook URL pasted into the alert's webhook field. The message can be plain text.&lt;/p&gt;

&lt;p&gt;Discord or Telegram: a channel in a server or group you run. It reads the posts, with an author or keyword filter so chatter does not trade. The post can name the contract itself.&lt;/p&gt;

&lt;p&gt;Anything that can POST to a URL: TrendSpider, a Zapier step, your own scanner.&lt;/p&gt;

&lt;p&gt;The alert for a stock, or for an options strategy that picks its own contract, is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPY long entry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the exit is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPY long exit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole template. Ticker, direction, action. JSON works too if your source speaks it, but for a chart alert I use text so I can read it in the TradingView alert log.&lt;/p&gt;

&lt;h2&gt;
  
  
  The strategy is a form, and the form decides four things
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;What each alert does. Four alert kinds: long entry, long exit, short entry, short exit. Under Long behavior and Short behavior you say whether an entry opens a position, closes the other side, or does nothing, and whether exits close. A long-only setup can treat short entries as its exits, which is handy when the indicator only knows "flip".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How the option contract is picked. "From my rules": one to four legs, each finding its strike by delta, by price, by strikes from the money, or by offset from another leg, with buy or sell, call or put, and a DTE range. "Provided in alert": the message names the strike and expiry (Discord, Telegram, your own bot).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Order type, duration and limit target. Market or limit, set separately for entries and exits on each side. A limit takes its price from Bid, Ask, Mid, a custom number, or the price in the alert. Duration is Day, GTC or FOK. Tradier has no FOK, so FOK goes there as Day.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trading window and stacking. "Limit trading hours" drops entries outside your window and still lets exits through. "Allow multiple entries" is off by default, so a second entry on an open position is skipped and logged, not stacked.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The bot is the strategy on one account with one budget
&lt;/h2&gt;

&lt;p&gt;Deploying a bot is: pick a connected account, set a budget per entry. That is it.&lt;/p&gt;

&lt;p&gt;Stocks size in whole shares. $500 on a $50 stock buys 10. Options size per contract, and a contract is 100 shares, so a $2.00 premium costs $200: a $500 budget buys 2 and leaves $100 unspent. If one contract costs more than the budget, the deploy is refused up front when a live quote shows it. You cannot deploy a bot that can never trade.&lt;/p&gt;

&lt;p&gt;The same strategy on a second account is a second bot, with its own stats.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real one, start to finish
&lt;/h2&gt;

&lt;p&gt;Strategy: options, From my rules, one leg: Buy, Call, 0.40 delta, 14 to 31 DTE. Long behavior: Enter position, with Close on exit alerts on. Entries limit at Ask, exits limit at Bid, Day. Bot on the built-in paper broker, $500 per entry.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SPY long entry&lt;/code&gt; arrives at 10:14. The bot picks the listed SPY call closest to 0.40 delta inside 14 to 31 DTE with a live quote. The ask is $2.00, so it buys 2. &lt;code&gt;SPY long exit&lt;/code&gt; at 2:50 sells the 2 it holds.&lt;/p&gt;

&lt;p&gt;What breaks, from my own logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No contract inside the DTE range within 0.25 of the target delta: entry skipped, nothing placed, the Logs tab says so.&lt;/li&gt;
&lt;li&gt;Options market closed: the options alert is refused. Stocks on the built-in paper broker fill at any hour, which makes an evening test look fine and a 9:25 AM live alert fail.&lt;/li&gt;
&lt;li&gt;An exit arrives and nothing is tracked as open, because the bot was paused or I closed by hand at the broker: logged as Skipped, not Failed. It tracks what it placed, not your whole account. Give it an account of its own.&lt;/li&gt;
&lt;li&gt;Two alerts 100 ms apart can arrive out of order. To flip, send one reversing alert, not an exit and then an entry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paper first, and what paper does not prove
&lt;/h2&gt;

&lt;p&gt;The built-in paper broker is free, up to 10 bots. A market order there fills at the quoted bid or ask: it pays the spread, never slips and never partially fills, so paper P&amp;amp;L reads better than live. What a paper run proves is that the alert fires, the text is read the way you meant, each exit closes its entry, and the quantity is what you expected. It cannot see a broker rejection such as an options approval level.&lt;/p&gt;

&lt;p&gt;For the broker's own rejections, connect the broker's paper account instead. Alpaca paper takes an API key generated with the dashboard toggled to Paper; keys start with PK, and a live AK key is refused on paper. Tradier sandbox takes the sandbox account number and token from developer.tradier.com, runs on 15-minute delayed data, and on weekends marks resting limit orders as filled at prices that never traded. Read Tradier sandbox results on weekday regular hours only.&lt;/p&gt;

&lt;p&gt;Run a week or two. Then deploy on the live account, or edit the bot to point at it. The first live deploy makes you accept the risk disclosure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it will not do
&lt;/h2&gt;

&lt;p&gt;Market and limit orders only. No stop or stop-limit, no bracket, no OCO, no trailing stop. A position closes when an exit alert arrives, or when an opposite entry is set to close it, and that is the entire exit model. No futures, no forex; an index alert trades as an option. No guessing at "thinking about SPY?", which places nothing. And no opinion on whether the strategy makes money.&lt;/p&gt;

&lt;p&gt;Live brokers: Charles Schwab, Tradier, tastytrade, Alpaca. Paper is free; live is from $15 a month. The strategy settings are documented &lt;a href="https://docs.nyria.io/strategies" rel="noopener noreferrer"&gt;here&lt;/a&gt;. To run the SPY example above on a paper bot, &lt;a href="https://app.nyria.io/login?mode=signup" rel="noopener noreferrer"&gt;start on Nyria&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nocode</category>
      <category>trading</category>
      <category>automation</category>
      <category>finance</category>
    </item>
    <item>
      <title>Send alerts from your Python script to your broker</title>
      <dc:creator>Nyria</dc:creator>
      <pubDate>Thu, 22 Oct 2026 19:47:43 +0000</pubDate>
      <link>https://dev.to/nyriaautomation/send-alerts-from-your-python-script-to-your-broker-1f8n</link>
      <guid>https://dev.to/nyriaautomation/send-alerts-from-your-python-script-to-your-broker-1f8n</guid>
      <description>&lt;p&gt;I run a couple of scanners in Python. For a long time their output was a Telegram message to myself, then me opening the broker app and typing the order by hand. The script knew the trade thirty seconds before I did, and I was still the slow part.&lt;/p&gt;

&lt;p&gt;Disclosure up front: I build Nyria, and the webhook in this post is its webhook. I also use it exactly this way, which is why the gotchas below are specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the script sends
&lt;/h2&gt;

&lt;p&gt;One HTTPS POST per alert, to a URL that belongs to one strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://trades.nyria.io/webhooks/{token}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token is 64 lowercase hex characters and it is the only authentication. No Authorization header, no HMAC signature, no IP allowlist. Treat the URL as a secret. If it ends up in a screenshot or a repo, rotate it from the strategy's Integration tab. The old URL dies instantly, so every sender you pointed at it needs the new one.&lt;/p&gt;

&lt;p&gt;The body is JSON. The smallest one that places a trade is two fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ticker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AAPL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buy"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I don't send that one from a script. &lt;code&gt;buy&lt;/code&gt; is read against the strategy's state: on a long-only strategy it opens a long, but on a strategy that trades both sides it covers an open short first and only otherwise opens a long. From code I want to say exactly what I mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ticker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AAPL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OPEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LONG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"order_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"market"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;action&lt;/code&gt; is OPEN or CLOSE (MODIFY and CANCEL exist for working limit orders). &lt;code&gt;direction&lt;/code&gt; is LONG or SHORT. &lt;code&gt;order_type&lt;/code&gt; is market or limit, and it is only read on an alert that names action and direction like this one; on anything else the strategy's default applies. &lt;code&gt;price&lt;/code&gt; is the limit price when you send limit.&lt;/p&gt;

&lt;p&gt;There is no quantity field. I tried. &lt;code&gt;quantity&lt;/code&gt; and &lt;code&gt;notional&lt;/code&gt; are ignored on an entry; size always comes from the bot's budget per entry. Duration (Day, GTC, FOK) is a strategy setting too, not an alert field. The one exception is MODIFY, which can re-size a working order on Tradier and Alpaca.&lt;/p&gt;

&lt;p&gt;Options are a separate case. If the strategy is set to "From my rules", the alert is the same as above and the strategy picks the strike and expiry. If it is set to "Provided in alert", you send a &lt;code&gt;legs&lt;/code&gt; array with symbol, side, action, strike and expiration per leg, up to 4 legs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sender
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;WEBHOOK_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://trades.nyria.io/webhooks/REPLACE_WITH_YOUR_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ticker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AAPL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;direction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LONG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alert accepted, handed to the bots&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not acted on:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good response looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"processed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"webhook"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"originalAlert"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"structuredAlert"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"instrument"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AAPL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OPEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LONG"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in that script I got wrong the first time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;raise_for_status()&lt;/code&gt; only catches 4xx and 5xx. A rejected alert and a duplicate both come back as 200 with a &lt;code&gt;status&lt;/code&gt; that is not &lt;code&gt;processed&lt;/code&gt;. Check &lt;code&gt;status&lt;/code&gt; too, or your script logs a green tick on a trade that never happened.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;timeout=10&lt;/code&gt;. The endpoint answers fast, but a hung socket on your side stalls the whole scanner loop and the next three alerts queue behind it.&lt;/p&gt;

&lt;p&gt;To close: same ticker, &lt;code&gt;"action": "CLOSE", "direction": "LONG"&lt;/code&gt;. It closes the whole tracked position. There is no partial close through the webhook.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the responses mean
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;200&lt;/code&gt; with &lt;code&gt;status: processed&lt;/code&gt;. Read and handed to the bots. Not a fill. The bot's Logs tab shows what the broker did.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;200&lt;/code&gt; with &lt;code&gt;rejected: true&lt;/code&gt;. The strategy is not Active (draft, paused, archived).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;200&lt;/code&gt; with &lt;code&gt;status: rejected&lt;/code&gt;. Ticker not in the strategy's list, that direction turned off, or nothing open to close. The reason is in the body.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;200&lt;/code&gt; with &lt;code&gt;status: duplicate&lt;/code&gt;. See below.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;400&lt;/code&gt; or &lt;code&gt;404&lt;/code&gt;. Bad or unknown token. Copy the URL again.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;413&lt;/code&gt;. Body over 256 KB.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;422&lt;/code&gt;. Unusable alert: missing field, unknown symbol, no strike in the DTE range, more than 4 legs. &lt;code&gt;userMessage&lt;/code&gt; says which.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;429&lt;/code&gt;. Burst limit is 10 at once, then 1 per second. Honor &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;503&lt;/code&gt;. Temporary, usually market data. Retrying is safe.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;500&lt;/code&gt;. Everything else. The strategy's Logs tab has the reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there is no row on the Logs tab at all, the request never arrived: wrong host, http instead of https, or a proxy ate it. One exception: a duplicate is only logged once every 5 minutes, so a missing row can also be a repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The duplicate rule, and the scale-in it ate
&lt;/h2&gt;

&lt;p&gt;An identical body within 30 seconds is a duplicate. So is an identical body within 5 minutes if nothing else came in between. Only the first one trades.&lt;/p&gt;

&lt;p&gt;That saved me when &lt;code&gt;requests&lt;/code&gt; retried after a flaky connection. It also silently dropped a second scale-in I sent 90 seconds after the first with the same payload. The response was 200 with &lt;code&gt;status: duplicate&lt;/code&gt;, which my script was not checking at the time.&lt;/p&gt;

&lt;p&gt;The fix is a unique &lt;code&gt;metadata.message_id&lt;/code&gt; per intended order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scalein-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dedup matches on that id when it is present, and on a hash of the whole body when it is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broker sandbox gotcha
&lt;/h2&gt;

&lt;p&gt;I test against three paper accounts and they behave differently, which matters when a test "passes".&lt;/p&gt;

&lt;p&gt;The built-in paper broker fills or refuses every order instantly. A limit order never rests there, so MODIFY and CANCEL have nothing to act on. Test those against a real broker's paper account.&lt;/p&gt;

&lt;p&gt;Alpaca paper: generate the keys with the dashboard toggled to Paper. A paper key starts with &lt;code&gt;PK&lt;/code&gt;, a live key with &lt;code&gt;AK&lt;/code&gt;, and each only works on its own endpoint. Paste a live key into the paper slot and the connection check fails; Alpaca answers 401 to a key on the wrong host. Outside market hours an Alpaca paper market order sits as &lt;code&gt;accepted&lt;/code&gt; until the open, which is correct, and which confused me the first Sunday I tried this.&lt;/p&gt;

&lt;p&gt;Tradier sandbox: it connects with the sandbox account number and access token from developer.tradier.com, not a tradier.com login. Its data is 15 minutes delayed, and on weekends it marks resting limit orders as filled at prices that never printed. A Friday 3:59 PM limit can show as filled on Saturday morning. The API reports it as a fill, so the bot records a fill and waits for an exit. Judge Tradier sandbox runs on weekday regular hours only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before the first real post
&lt;/h2&gt;

&lt;p&gt;Paste the body into the &lt;a href="https://nyria.io/tools/alert-tester" rel="noopener noreferrer"&gt;alert tester&lt;/a&gt;. It shows the order it would build, or the exact refusal text. No account, nothing traded. Then paste the same body on the strategy's Integration tab as the example for that trade type and select Check format. The strategy only goes Active after that passes, and an inactive strategy answers every post with rejected.&lt;/p&gt;

&lt;p&gt;Then point the script at a paper bot, send one OPEN, open the bot, and look at the quantity on the order. That number is your budget per entry divided by the price, floored to whole shares. If it is what you expected, you are done.&lt;/p&gt;

&lt;p&gt;Every field and a curl example per broker is in the &lt;a href="https://docs.nyria.io/webhook-reference" rel="noopener noreferrer"&gt;webhook payload reference&lt;/a&gt;. Paper bots are free, up to 10, no card. &lt;a href="https://app.nyria.io/login?mode=signup" rel="noopener noreferrer"&gt;Start on Nyria&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webhooks</category>
      <category>trading</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
