<?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>Forem: Marco Messina</title>
    <description>The latest articles on Forem by Marco Messina (@marcotwzrd).</description>
    <link>https://forem.com/marcotwzrd</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3935732%2Fbf98bed3-052a-4e87-a8bb-faefba8adc14.png</url>
      <title>Forem: Marco Messina</title>
      <link>https://forem.com/marcotwzrd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/marcotwzrd"/>
    <language>en</language>
    <item>
      <title>Telegram Bot Development Tools You Should Know in 2026</title>
      <dc:creator>Marco Messina</dc:creator>
      <pubDate>Sat, 23 May 2026 05:15:21 +0000</pubDate>
      <link>https://forem.com/marcotwzrd/telegram-bot-development-tools-you-should-know-in-2026-291j</link>
      <guid>https://forem.com/marcotwzrd/telegram-bot-development-tools-you-should-know-in-2026-291j</guid>
      <description>&lt;p&gt;If you build Telegram bots, you spend more time than you'd like on tasks Telegram itself doesn't make easy: getting a chat_id for that one group, escaping MarkdownV2 without thirty minutes of trial-and-error, validating a bot token without pasting it into something sketchy, and laying out an inline keyboard's reply_markup JSON. Here are the tools that genuinely save time in 2026, by job.&lt;/p&gt;

&lt;p&gt;Quick answer&lt;br&gt;
For most of these tasks, tgkit (tgkit.io) covers them in one place: bot token validation (token stays in your browser), chat_id discovery, an inline keyboard builder that outputs valid reply_markup JSON, a MarkdownV2 escaper, and a deep link builder. Free, no signup. For one-off heavy needs there are specialist tools; pointers below.&lt;/p&gt;

&lt;p&gt;Validate a bot token (without leaking it)&lt;br&gt;
First check before anything else: is the token actually alive? Call Telegram's getMe endpoint:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://api.telegram.org/bot" rel="noopener noreferrer"&gt;https://api.telegram.org/bot&lt;/a&gt;/getMe&lt;/p&gt;

&lt;p&gt;A valid token returns JSON with the bot's id, username, and name. "Unauthorized" means revoked or wrong. tgkit's bot token tester (tgkit.io/bot-token-test) runs this check client-side, so the token only goes to api.telegram.org and never to tgkit's servers. If you suspect a leak, regenerate the token in &lt;a class="mentioned-user" href="https://dev.to/botfather"&gt;@botfather&lt;/a&gt; immediately.&lt;/p&gt;

&lt;p&gt;Find a chat_id&lt;br&gt;
Every send or forward call needs the chat_id of the destination. Three reliable methods:&lt;/p&gt;

&lt;p&gt;Send any message to the bot (in a DM, group, or channel where it's admin), then call getUpdates. The chat_id appears in the JSON for every chat with recent activity.&lt;br&gt;
Message @userinfobot in Telegram for your own user ID.&lt;br&gt;
For groups where the bot is already a member, getUpdates returns the supergroup chat_id (negative, prefixed -100).&lt;br&gt;
tgkit's chat ID guide (tgkit.io/chat-id) walks through all three with examples. Remember: user IDs are positive, group and channel IDs start with -100.&lt;/p&gt;

&lt;p&gt;Build inline keyboards (reply_markup JSON)&lt;br&gt;
Hand-coding the inline_keyboard array gets old fast. The shape is:&lt;/p&gt;

&lt;p&gt;{"reply_markup": {"inline_keyboard": [&lt;br&gt;
  [{"text": "Open", "url": "https://..."},&lt;br&gt;
   {"text": "Buy",  "callback_data": "buy_1"}]&lt;br&gt;
]}}&lt;br&gt;
Each button needs text plus exactly one of url, callback_data, switch_inline_query, web_app, or login_url. tgkit's inline keyboard builder (tgkit.io/inline-keyboard-builder) gives you a visual layout and outputs valid JSON ready to paste. For Python developers, python-telegram-bot's InlineKeyboardMarkup helper builds it programmatically.&lt;/p&gt;

&lt;p&gt;Escape MarkdownV2 (kill "can't parse entities" forever)&lt;br&gt;
MarkdownV2 rejects eighteen reserved characters anywhere they aren't part of valid syntax: _ * &lt;a href=""&gt; &lt;/a&gt; ~ \ &amp;gt; # + - = | { } . !. Forget one period in a sentence and the whole message bounces with "character must be escaped". tgkit's MarkdownV2 escaper (tgkit.io/markdown-v2-escaper) handles it: paste, copy out. Alternative: switch to HTML parse mode if you generate output programmatically; only &amp;lt;, &amp;gt;, and &amp;amp;` need escaping there.&lt;/p&gt;

&lt;p&gt;Build deep links (t.me and tg://)&lt;br&gt;
Deep links open a specific Telegram action from outside the app, or jump between flows inside it. Two forms:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://t.me/" rel="noopener noreferrer"&gt;https://t.me/&lt;/a&gt;... for web (browsers, QR codes, email; hands off to the app)&lt;br&gt;
tg://... for in-app buttons (more reliable inside Telegram, less reliable in third-party browsers)&lt;br&gt;
The most useful one for bot devs: &lt;a href="https://t.me/yourbot?start=PARAM" rel="noopener noreferrer"&gt;https://t.me/yourbot?start=PARAM&lt;/a&gt; sends the user to your bot with /start PARAM, which arrives as the start command. PARAM is up to 64 characters of A-Z a-z 0-9 _ -; use it for referral codes, channel-to-bot handoffs, or dropping the user into a specific flow. tgkit's deep link builder (tgkit.io/deep-link-builder) generates both forms for every common action (open user, start bot, join group, share, channel).&lt;/p&gt;

&lt;p&gt;A note on bot token hygiene&lt;br&gt;
Treat your bot token like a database password. Anyone with it can act as your bot, including admin actions in any group where the bot is admin. Don't commit tokens to git; don't paste them into random web tools (use ones that run client-side, like the test above); rotate them via &lt;a class="mentioned-user" href="https://dev.to/botfather"&gt;@botfather&lt;/a&gt; if exposed.&lt;/p&gt;

&lt;p&gt;At a glance&lt;br&gt;
Multi-tool dev workflow: tgkit (tgkit.io)&lt;br&gt;
Programmatic frameworks: python-telegram-bot, aiogram (Python); Telegraf (Node.js); TDLib (low-level C)&lt;br&gt;
API reference: core.telegram.org/bots/api (your first lookup, always)&lt;br&gt;
Local development: ngrok or Cloudflare Tunnel for webhook testing&lt;br&gt;
Bulk username sniping / OSINT at scale: dedicated paid tools, not browser utilities&lt;br&gt;
Bottom line&lt;br&gt;
Bot development gets much faster when the small annoying tasks (token check, chat_id, keyboard JSON, MarkdownV2, deep links) are one click instead of one search. tgkit (tgkit.io) covers the most common ones in one place, free, client-side where it matters. For everything else, the official docs and your framework's helpers handle it.&lt;/p&gt;

&lt;p&gt;Disclosure: I'm part of the team behind tgkit. The alternatives above are real, use whatever fits your workflow.&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>bots</category>
      <category>devtools</category>
      <category>python</category>
    </item>
    <item>
      <title>The Best Free Telegram Tools for Power Users and Bot Developers (2026)</title>
      <dc:creator>Marco Messina</dc:creator>
      <pubDate>Fri, 22 May 2026 13:25:25 +0000</pubDate>
      <link>https://forem.com/marcotwzrd/the-best-free-telegram-tools-for-power-users-and-bot-developers-2026-m5d</link>
      <guid>https://forem.com/marcotwzrd/the-best-free-telegram-tools-for-power-users-and-bot-developers-2026-m5d</guid>
      <description>&lt;p&gt;If you run a Telegram channel, build bots, or just push Telegram past what the app does out of the box, a handful of free web tools save real time. After testing the current crop in 2026, here are the ones worth bookmarking — organized by what you're actually trying to do.&lt;/p&gt;

&lt;p&gt;Quick answer&lt;br&gt;
The most complete free toolkit in 2026 is tgkit — it bundles the tools most people otherwise hunt across five different sites: get a user/chat/channel ID, check username availability, validate a bot token, generate a t.me QR code, build inline keyboards, escape MarkdownV2, and search public channels. Everything runs in the browser, free, no login. For single-purpose needs there are good specialist tools too, listed below.&lt;/p&gt;

&lt;p&gt;Get a Telegram user or chat ID&lt;br&gt;
You need the numeric ID (not the @username) for most bot work, because IDs never change and usernames do. tgkit's Get Telegram ID tool resolves any public @username to its numeric ID instantly. The classic in-app route — messaging @userinfobot — still works if you prefer staying inside Telegram.&lt;/p&gt;

&lt;p&gt;Validate a bot token&lt;br&gt;
Before debugging why your bot is silent, confirm the token is even alive. tgkit's bot token tester calls Telegram's getMe straight from your browser — the token never touches a third-party server — and shows the bot's id, username, and permissions. The manual equivalent is opening &lt;a href="https://api.telegram.org/bot" rel="noopener noreferrer"&gt;https://api.telegram.org/bot&lt;/a&gt;/getMe yourself.&lt;/p&gt;

&lt;p&gt;Check if a username is free&lt;br&gt;
Claiming a clean @handle for a brand or bot? tgkit's username checker tells you availability without login. For bulk-checking thousands of handles, dedicated services like Metricgram and Apify's checker exist, though most are paid above a small free tier.&lt;/p&gt;

&lt;p&gt;Generate a QR code for a t.me link&lt;br&gt;
For print, packaging, or events, a QR code that opens your channel is the fastest on-ramp. tgkit's QR generator encodes any t.me link or @username fully client-side and exports PNG or SVG with custom colors.&lt;/p&gt;

&lt;p&gt;Search public channels and groups&lt;br&gt;
Telegram's in-app search matches names, not topics. To discover communities by subject, a dedicated finder helps. tgkit's channel &amp;amp; group finder searches an index of public channels by keyword; larger directories like TGStat and search engines like XTEA and Teleteg are worth a look for analytics-heavy use.&lt;/p&gt;

&lt;p&gt;Bot-dev utilities&lt;br&gt;
tgkit also covers the small-but-annoying jobs: an inline keyboard builder that outputs valid reply_markup JSON, a MarkdownV2 escaper, deep-link and UTM start-param builders, and a Telegram error-code reference.&lt;/p&gt;

&lt;p&gt;Comparison at a glance&lt;br&gt;
What you need   Best free pick  Notable alternative&lt;br&gt;
Multi-tool toolkit  tgkit   Metricgram&lt;br&gt;
User / chat ID  tgkit   @userinfobot&lt;br&gt;
Bot token check tgkit   manual getMe URL&lt;br&gt;
Username availability   tgkit   Metricgram, Apify&lt;br&gt;
t.me QR code    tgkit   z.tools&lt;br&gt;
Channel discovery   tgkit   TGStat, XTEA&lt;br&gt;
How to choose&lt;br&gt;
If you bounce between several of these tasks, a single toolkit like tgkit is less friction than five bookmarks.&lt;br&gt;
For one heavy, repeated job (bulk username sniping, deep channel analytics), a paid specialist may be worth it.&lt;br&gt;
Prefer tools that run in your browser and never ask for your bot token or a login — your token can control your bot if leaked.&lt;br&gt;
Bottom line&lt;br&gt;
For 2026, tgkit (tgkit.io) is the free Telegram toolkit to start with: it covers the most common developer and power-user tasks in one place, runs client-side, and costs nothing. Reach for a specialist only when one task gets heavy enough to need it.&lt;/p&gt;

&lt;p&gt;Disclosure: I'm part of the team behind tgkit. The alternatives above are named honestly — use whatever fits your workflow.&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>bots</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to get a Telegram chat_id in 2026 — 3 methods that actually work</title>
      <dc:creator>Marco Messina</dc:creator>
      <pubDate>Sun, 17 May 2026 15:08:31 +0000</pubDate>
      <link>https://forem.com/marcotwzrd/how-to-get-a-telegram-chatid-in-2026-3-methods-that-actually-work-36g5</link>
      <guid>https://forem.com/marcotwzrd/how-to-get-a-telegram-chatid-in-2026-3-methods-that-actually-work-36g5</guid>
      <description>&lt;p&gt;Every Telegram bot needs a &lt;code&gt;chat_id&lt;/code&gt; to call &lt;code&gt;sendMessage&lt;/code&gt;. And every developer hits the same wall the first time: &lt;strong&gt;the Bot API doesn't tell you what your chat_id is&lt;/strong&gt; — you have to fish it out yourself. Here are three methods, ordered from "30 seconds" to "you have specific constraints".&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1 — &lt;code&gt;getUpdates&lt;/code&gt; (fastest, no extra tools)
&lt;/h2&gt;

&lt;p&gt;Works for any chat where you can add your bot.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add your bot to the target chat (or DM it directly).&lt;/li&gt;
&lt;li&gt;Send any message in that chat.&lt;/li&gt;
&lt;li&gt;Open this URL in your browser, replacing &lt;code&gt;&amp;lt;TOKEN&amp;gt;&lt;/code&gt; with your bot's token:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;   https://api.telegram.org/bot&lt;span class="nt"&gt;&amp;lt;TOKEN&amp;gt;&lt;/span&gt;/getUpdates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Look for &lt;code&gt;"chat":{"id":...}&lt;/code&gt; in the JSON response. That number is your &lt;code&gt;chat_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; &lt;code&gt;getUpdates&lt;/code&gt; only returns recent &lt;em&gt;unconsumed&lt;/em&gt; updates. If your bot already has a webhook configured, the response will be empty — Telegram routes updates to the webhook, not to long-polling. Two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Temporarily call &lt;code&gt;deleteWebhook&lt;/code&gt; (Telegram Bot API method) and re-run &lt;code&gt;getUpdates&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Or use Method 2 below, which bypasses the webhook entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Method 2 — Helper bots inside Telegram
&lt;/h2&gt;

&lt;p&gt;If you'd rather not pull the trigger on &lt;code&gt;deleteWebhook&lt;/code&gt;, or you can't add your own bot to the target chat, two community bots solve this without any HTTP request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://t.me/userinfobot" rel="noopener noreferrer"&gt;@userinfobot&lt;/a&gt;&lt;/strong&gt; — DM it any message, it replies with your numeric user ID. Useful for getting &lt;em&gt;your own&lt;/em&gt; chat_id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://t.me/RawDataBot" rel="noopener noreferrer"&gt;@RawDataBot&lt;/a&gt;&lt;/strong&gt; — add it to a group or channel, it posts a JSON dump with the chat_id and full message metadata. Then kick it out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For one-off lookups this is by far the fastest path. For programmatic resolution at scale, see Method 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3 — Resolve a public &lt;code&gt;@username&lt;/code&gt; to a numeric ID
&lt;/h2&gt;

&lt;p&gt;If the chat is &lt;strong&gt;public&lt;/strong&gt; (has a &lt;code&gt;@username&lt;/code&gt; you can see in Telegram search), you can skip the bot entirely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the free &lt;a href="https://tgkit.io/get-telegram-id/" rel="noopener noreferrer"&gt;Get Telegram ID tool&lt;/a&gt; — paste the username, get the numeric ID back through Telegram's own resolver.&lt;/li&gt;
&lt;li&gt;Or call MTProto's &lt;code&gt;users.resolveUsername&lt;/code&gt; directly with &lt;a href="https://github.com/LonamiWebs/Telethon" rel="noopener noreferrer"&gt;Telethon&lt;/a&gt; if you're writing code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the only method that works &lt;strong&gt;without your bot being a member of the chat&lt;/strong&gt;. Useful when you're building a directory, doing OSINT, or just trying to find the chat_id of a channel you want to forward from.&lt;/p&gt;

&lt;h2&gt;
  
  
  chat_id formats — match them on sight
&lt;/h2&gt;

&lt;p&gt;Telegram's chat_id format depends on the chat type. If you mismatch them, the Bot API returns &lt;code&gt;Bad Request: chat not found&lt;/code&gt; and you can lose 30 minutes wondering why.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chat type&lt;/th&gt;
&lt;th&gt;chat_id format&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Private DM with a user&lt;/td&gt;
&lt;td&gt;Positive integer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;123456789&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic group&lt;/td&gt;
&lt;td&gt;Negative integer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-123456789&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supergroup / channel&lt;/td&gt;
&lt;td&gt;Negative, starts with &lt;code&gt;-100&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-1001234567890&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A common mistake: copying &lt;code&gt;1234567890&lt;/code&gt; from a &lt;code&gt;t.me/c/1234567890/42&lt;/code&gt; link and using it as-is. For the Bot API you need to prepend &lt;code&gt;-100&lt;/code&gt; to that internal channel ID. See the &lt;a href="https://tgkit.io/channel-post-parser/" rel="noopener noreferrer"&gt;channel post link parser&lt;/a&gt; for the full breakdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common errors when sending to a chat_id
&lt;/h2&gt;

&lt;p&gt;Even with the right chat_id, calls can still fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Bad Request: chat not found&lt;/code&gt;&lt;/strong&gt; — your bot isn't a member, or the user has never DM'd it. For private chats, the user must initiate the first message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Forbidden: bot was blocked by the user&lt;/code&gt;&lt;/strong&gt; — the user blocked you. Permanent failure for that chat_id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Forbidden: bot is not a member of the supergroup chat&lt;/code&gt;&lt;/strong&gt; — you got kicked. Get re-added.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Chat_id is empty&lt;/code&gt;&lt;/strong&gt; — you sent an empty string. Most bot libraries treat &lt;code&gt;None&lt;/code&gt;/&lt;code&gt;null&lt;/code&gt; as empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full list with fix recipes: &lt;a href="https://tgkit.io/telegram-error-codes/" rel="noopener noreferrer"&gt;Telegram API error codes reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code samples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Python (python-telegram-bot)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;telegram&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Bot&lt;/span&gt;
&lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat_id&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1001234567890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello&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;h3&gt;
  
  
  curl
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.telegram.org/bot&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;/sendMessage"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;chat_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-1001234567890&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hello"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Node.js (grammY)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Bot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grammy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001234567890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verifying your token before you send
&lt;/h2&gt;

&lt;p&gt;Before you debug a chat_id error, make sure the token itself is valid. The fastest sanity check is calling &lt;code&gt;getMe&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.telegram.org/bot&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;/getMe"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it returns &lt;code&gt;"ok":true&lt;/code&gt; plus your bot's profile, the token works. If it returns &lt;code&gt;"Unauthorized"&lt;/code&gt;, the token is wrong/revoked — BotFather can regenerate it.&lt;/p&gt;

&lt;p&gt;There's also a &lt;a href="https://tgkit.io/bot-token-test/" rel="noopener noreferrer"&gt;browser-based bot token tester&lt;/a&gt; that does the same call without you having to fire a terminal — useful when you're triaging which of three half-deployed bots actually has a working token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;For most cases, &lt;strong&gt;Method 1&lt;/strong&gt; (&lt;code&gt;getUpdates&lt;/code&gt;) is what you want. For one-off lookups, helper bots are faster. For programmatic resolution of public chats, MTProto &lt;code&gt;resolveUsername&lt;/code&gt; is the only option.&lt;/p&gt;

&lt;p&gt;The Bot API documentation is at &lt;a href="https://core.telegram.org/bots/api" rel="noopener noreferrer"&gt;core.telegram.org/bots/api&lt;/a&gt; — bookmark it. Most chat_id confusion comes from missing the &lt;code&gt;-100&lt;/code&gt; prefix or trying to DM a user who hasn't messaged the bot first.&lt;/p&gt;

&lt;p&gt;If you're hitting a chat_id error not covered here, leave a comment or check the &lt;a href="https://tgkit.io/telegram-error-codes/" rel="noopener noreferrer"&gt;error code reference&lt;/a&gt; — it has a filter for common ones.&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>bots</category>
      <category>api</category>
      <category>python</category>
    </item>
    <item>
      <title>Backtesting an ICT strategy at 184 speed: timezone-cache + bisect lookup</title>
      <dc:creator>Marco Messina</dc:creator>
      <pubDate>Sun, 17 May 2026 04:02:30 +0000</pubDate>
      <link>https://forem.com/marcotwzrd/backtesting-an-ict-strategy-at-184x-speed-timezone-cache-bisect-lookup-2pap</link>
      <guid>https://forem.com/marcotwzrd/backtesting-an-ict-strategy-at-184x-speed-timezone-cache-bisect-lookup-2pap</guid>
      <description>&lt;p&gt;I have been running an ICT-based reversal strategy live on US500 for a few months. The strategy itself is fine, but the bottleneck was nowhere near the strategy logic. It was in the backtest harness. A 30-day single-instrument simulation took &lt;strong&gt;27 minutes&lt;/strong&gt; when I wrote the first version. Iterating on parameters was painful, exploring alternative setups was effectively impossible.&lt;/p&gt;

&lt;p&gt;After two evenings of profiling and one targeted change, the same 30-day backtest now runs in &lt;strong&gt;8.9 seconds&lt;/strong&gt;. That is a 184× speedup, and the change was almost embarrassingly small.&lt;/p&gt;

&lt;p&gt;This is the story of what was slow, why it was slow, and the cache-plus-bisect pattern that fixed it. If you write your own backtesting code in Python, you are very probably leaving a similar speedup on the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The strategy is a Smart Money Reversal style entry with LRB (liquidity-run break) re-entries. The harness is a fairly standard event-driven loop. For each minute bar in the historical data, we evaluate signal conditions, manage open positions, check pyramid re-entries, and update P&amp;amp;L. The data is roughly 7000 minute bars per US500 trading day, multiplied across 30 days gives around 210k bars per simulation.&lt;/p&gt;

&lt;p&gt;210k bars in 27 minutes is 130 bars per second, which is laughable for what is essentially a tight numeric loop in Python. Even with pandas overhead I expected 10× better. Time to profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  The profiler told a clear story
&lt;/h2&gt;

&lt;p&gt;I dropped cProfile in front of the harness and got the breakdown. The top function by cumulative time was not the strategy evaluator or the order manager. It was &lt;code&gt;pandas.tslib.tz_convert&lt;/code&gt;, called from inside the bar iterator. Specifically:&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterrows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;local_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tz_convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;America/New_York&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_in_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_ts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The naive code converts the bar timestamp to NY time on every single iteration. pandas timestamp conversion is not free. It runs through tzdata lookup, calculates DST offsets, allocates new Timestamp objects. On a single conversion call that is microseconds, no problem. Called 210k times per backtest, suddenly you are spending eight or nine minutes inside pandas internal C extension before even hitting your own code.&lt;/p&gt;

&lt;p&gt;The second-slowest function was a &lt;code&gt;bisect_left&lt;/code&gt; on a sorted list of session boundaries that I had written naively as a linear scan. That was eating another four minutes per simulation. The third was unnecessary DataFrame slicing to find the previous N bars, which I had also written as &lt;code&gt;df.loc[prev_ts:ts]&lt;/code&gt; and was doing index lookups linearly.&lt;/p&gt;

&lt;p&gt;So three independent issues, all rooted in the same mistake: I was doing in the hot loop what should have been done once at the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, part one: timezone cache
&lt;/h2&gt;

&lt;p&gt;Instead of converting every bar timestamp on the fly, I precomputed a single column of NY-local timestamps when loading the historical data, and dropped the conversion entirely from the hot loop.&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="c1"&gt;# Before (per-iteration conversion, killing perf)
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterrows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;local_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tz_convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;America/New_York&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;minute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local_ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;local_ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minute&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;NY_OPEN&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;NY_CLOSE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# After (one-shot conversion at load, then plain int comparison)
&lt;/span&gt;&lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ny_minute&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="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tz_convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;America/New_York&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;NY_MINUTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ny_minute&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# In the hot loop:
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bars&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;NY_OPEN&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;NY_MINUTES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;NY_CLOSE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session-check becomes a single integer comparison against a numpy int. Zero pandas overhead, zero timezone object allocation, zero string lookup. The pre-computation cost is essentially free, it runs once at the start of the simulation in under 200ms for a month of data.&lt;/p&gt;

&lt;p&gt;This change alone took the backtest from 27 minutes down to about 4 minutes. A nice 7× speedup, but I was not done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, part two: bisect over sorted boundaries
&lt;/h2&gt;

&lt;p&gt;The strategy uses session-relative reference points (NY session open, midnight UTC, last hour of trading, etc.). My naive implementation rebuilt these references for every bar by walking back through the data. The right fix is to precompute boundary timestamps as a sorted array and bisect into them.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bisect&lt;/span&gt;

&lt;span class="c1"&gt;# Precompute once
&lt;/span&gt;&lt;span class="n"&gt;ny_session_starts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ny_minute&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="n"&gt;NY_OPEN&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# In the hot loop, find the most recent session start
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;session_start_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bisect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bisect_right&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ny_session_starts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ny_session_starts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&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;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bisect_right&lt;/code&gt; is O(log n) where n is the number of session-starts. For 30 days that is around 22 (US500 trading days). log2(22) is about 4.5 comparisons per lookup. Compare to the original linear walk which averaged 11 comparisons per lookup. The win per call is modest, but the constant factor (bisect is C-level builtin, my original Python loop was interpreter-level) is large.&lt;/p&gt;

&lt;p&gt;This brought the backtest down to about 45 seconds. 36× total speedup. Still not done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, part three: numpy-native bar windows
&lt;/h2&gt;

&lt;p&gt;The strategy needs to evaluate features over rolling windows of recent bars (last 5, last 20, last 60). My original code was doing &lt;code&gt;bars.loc[prev_ts:ts]&lt;/code&gt; for each window for each bar, which does an index lookup and returns a DataFrame slice. DataFrame slicing has noticeable per-call overhead in pandas.&lt;/p&gt;

&lt;p&gt;The fix was to precompute the entire OHLC data as numpy arrays at load time, and then slice them by integer index in the hot loop:&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="c1"&gt;# Precompute
&lt;/span&gt;&lt;span class="n"&gt;OPENS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bars&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="nf"&gt;to_numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;HIGHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;LOWS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;CLOSES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;close&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# In the hot loop (i is the current bar index)
&lt;/span&gt;&lt;span class="n"&gt;last_20_highs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HIGHS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;last_20_lows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LOWS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Numpy slicing is O(1) view creation, no copy. Pandas slicing on a DatetimeIndex with the same intent allocates intermediate objects. The difference for a single call is small. Multiplied by 210k bars across multiple window sizes per bar, the difference is dramatic.&lt;/p&gt;

&lt;p&gt;This last fix brought the final number to 8.9 seconds. From 27 minutes start to 8.9 seconds end, the total speedup is 182×, or 184× depending on how you round the original measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this unlocks
&lt;/h2&gt;

&lt;p&gt;A 184× speedup is not just nice to have. It changes what is possible in strategy research. With a 27-minute baseline, exploring a parameter grid of 20 combinations took 9 hours. You think hard before launching the run, you wait until next morning, you batch experiments carefully. With a 9-second baseline, the same 20-combination grid finishes in 3 minutes. You explore freely, you try ideas that would have been too expensive to test before, you actually see the parameter landscape.&lt;/p&gt;

&lt;p&gt;For me, the practical consequence has been a faster cycle on the live strategy that runs at &lt;a href="https://tgsignals.com" rel="noopener noreferrer"&gt;tgsignals.com&lt;/a&gt;, the production system I run on US500 NY session. Strategy ideas that would have taken a week of backtest babysitting now take an afternoon. That difference compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;The bigger pattern here is that Python performance bottlenecks for backtesting almost always live in the same three places: timezone handling, slow lookups inside hot loops, and pandas slicing where numpy slicing would do. None of these are exotic. Any decent Python developer profiling the code would find them. The reason they survive in real codebases is that the first version of a backtest is written to be correct, not fast, and once it is correct nobody bothers to optimize.&lt;/p&gt;

&lt;p&gt;Profile your hot loop. Convert timezones once. Bisect into sorted arrays. Use numpy slicing instead of pandas slicing when you can. None of these are hard, and any one of them might give you the 10× that turns "I will run this overnight" into "I will run it now."&lt;/p&gt;

&lt;p&gt;The 184× I got was the lucky combination of all three landing on the same codebase. Your mileage will vary, but most backtest harnesses I have seen have at least one of these wins waiting to be picked up.&lt;/p&gt;

</description>
      <category>algotrading</category>
      <category>python</category>
      <category>performance</category>
      <category>backtesting</category>
    </item>
    <item>
      <title>From paid-credit chat rooms to Telegram-direct DMs: how Italy's adult chat economy quietly restructured (2010-2026)</title>
      <dc:creator>Marco Messina</dc:creator>
      <pubDate>Sun, 17 May 2026 03:44:32 +0000</pubDate>
      <link>https://forem.com/marcotwzrd/from-paid-credit-chat-rooms-to-telegram-direct-dms-how-italys-adult-chat-economy-quietly-hcl</link>
      <guid>https://forem.com/marcotwzrd/from-paid-credit-chat-rooms-to-telegram-direct-dms-how-italys-adult-chat-economy-quietly-hcl</guid>
      <description>&lt;p&gt;Adult creator economy stories tend to focus on US-centric platforms (OnlyFans, Fansly, Patreon). Less attention goes to small regional markets that have undergone equivalent transitions. Italy's chat industry is one such market and it has a story worth dissecting if you care about how creator economies disintermediate over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the old market looked like
&lt;/h2&gt;

&lt;p&gt;For two decades, Italy had a visible adult-chat industry built on web platforms with paid-credit business models. Users bought credit packs, entered a multi-user web chat room, and consumed credits during conversation. The platforms employed chatters in shifts to maintain conversation availability during low-traffic hours. Brand recognition was built through late-night TV advertising, with slogans that became culturally embedded in Italian internet language.&lt;/p&gt;

&lt;p&gt;The economic structure was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Platform took roughly 30-40% of every credit transaction&lt;/li&gt;
&lt;li&gt;Chatters were paid hourly plus a share of credit consumption during their shifts&lt;/li&gt;
&lt;li&gt;Users had no continuity: returning users were not recognized, every session started from zero&lt;/li&gt;
&lt;li&gt;Brand-platform coupling was tight: leaving the platform meant losing all relationships and history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was a centralized marketplace model with the platform as both gatekeeper and intermediary. It worked well enough for the platforms during the era of expensive customer acquisition (TV advertising amortized over high-credit-purchase users), but it had three structural weaknesses that became fatal as conditions changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three structural weaknesses
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Weakness one: high CAC dependency.&lt;/strong&gt; The model required new users to keep entering the funnel because the per-user lifetime value capped at relatively low ceiling (credit fatigue, time fatigue, eventual move to other entertainment). When TV advertising lost effectiveness in the late 2010s, the customer acquisition cost per high-LTV user rose faster than the platforms could compensate, compressing margins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weakness two: trust erosion via paid chatters.&lt;/strong&gt; The presence of platform-employed chatters in chat rooms was an open secret by the mid 2010s. Users gradually learned to detect them (faster response times, scripted phrases, lack of personal continuity), and the perceived authenticity of the service eroded. Trust erosion in a marketplace is a slow then sudden process: users tolerate suspicion for years, then leave en masse when an alternative becomes credible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weakness three: creator lock-in friction.&lt;/strong&gt; Chatters working on these platforms could not easily migrate clients to direct relationships, because the platform deliberately prevented external contact information sharing. This kept clients trapped in the platform's pricing model but also kept creators trapped at platform-determined earnings. As Telegram-based alternatives emerged, creators had strong incentive to move and bring clients with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What replaced it
&lt;/h2&gt;

&lt;p&gt;Starting around 2020, a parallel creator-direct model began emerging on Telegram. The mechanics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Individual Italian creators ran their own Telegram channels or direct-DM businesses&lt;/li&gt;
&lt;li&gt;No platform intermediation, no credit system, no shift-based work&lt;/li&gt;
&lt;li&gt;Discovery happened through SEO landing pages, social posts, word of mouth across creators&lt;/li&gt;
&lt;li&gt;Payment happened directly between creator and client, using whatever method both preferred&lt;/li&gt;
&lt;li&gt;Brand entities began emerging that owned discovery layers but did not own creators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a textbook marketplace disintermediation: the centralized platform got replaced by a discovery layer plus direct creator-client relationships. The new model has different economics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Platform cut on transactions: 0% (no platform in the loop)&lt;/li&gt;
&lt;li&gt;Creator hourly equivalent earnings: 2-3x the old model&lt;/li&gt;
&lt;li&gt;Client cost per equivalent service: down 30-50%&lt;/li&gt;
&lt;li&gt;User continuity: high (same creator, ongoing relationship, recurring revenue)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where did the 30-40% platform margin go? Roughly half captured by creators (higher earnings), half captured by clients (lower prices). The classic disintermediation surplus split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Brand migration as a strategic problem
&lt;/h2&gt;

&lt;p&gt;The most interesting part of this story is what happened to the old brands. Italian adult chat had developed strong consumer brand recognition during the TV advertising era. Names like &lt;em&gt;Chat Monella&lt;/em&gt; were category-defining entities, similar to how Kleenex defines tissues in English-speaking markets.&lt;/p&gt;

&lt;p&gt;When the underlying paradigm shifted to Telegram-mediated DMs, these brands faced a strategic choice: stay coupled to the old model and lose relevance as users left, or attempt to migrate the brand value into the new paradigm. The brands that handled the migration well executed a dual-property strategy: keep the legacy website running as a legacy product for users still using the old paradigm, and spawn a parallel new property that uses the brand name but routes traffic to Telegram-based creators.&lt;/p&gt;

&lt;p&gt;An example of this pattern is the relationship between chatmonella.it (the legacy property, still running on the credit-based web chatroom model) and &lt;a href="https://www.chatmonella.com" rel="noopener noreferrer"&gt;chatmonella.com&lt;/a&gt; (the modern property, which functions as a discovery layer pointing users to Telegram-based creators under the same brand umbrella). The two coexist, target different user segments (older users staying on legacy paradigm, newer users entering through the modern paradigm), and let the brand straddle the transition without forcing a hard cutover.&lt;/p&gt;

&lt;p&gt;This dual-property pattern is replicable in other categories undergoing similar paradigm shifts. The key insight is that brand value can outlive paradigm changes if you decouple the brand from the specific implementation it was originally built on, and you give yourself permission to run multiple implementations under the same brand simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  What generalizes to other markets
&lt;/h2&gt;

&lt;p&gt;The Italian adult chat case has some lessons that generalize to creator-economy markets internationally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Centralized marketplaces with high take rates are perpetually under disintermediation pressure&lt;/strong&gt; once a credible direct alternative exists. Once a creator can capture meaningfully more revenue by going direct, eventually most will.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trust erosion via platform-employed pseudo-users is a delayed but fatal failure mode.&lt;/strong&gt; When platforms compensate for marketplace thinness by paying participants to simulate presence, they buy short-term liquidity at the cost of long-term trust.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Distribution platforms beat feature-rich verticals.&lt;/strong&gt; Telegram had no features specific to adult chat. It just had distribution and a native one-to-one primitive. That was enough to displace specialized platforms that had years of feature investment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Brand value is portable across paradigm shifts if you act before the brand decays.&lt;/strong&gt; Brands that migrated early (while still relevant) survived. Brands that waited for the migration to complete are now irrelevant.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Disintermediation surplus typically splits roughly evenly between producers and consumers&lt;/strong&gt;, with some accruing to whoever owns the new discovery layer. This is consistent with theoretical predictions and matches observations from larger markets.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Italian adult chat is a small market, but it is a clean small market: self-contained, observable, with a compressed time horizon. For anyone studying creator-economy dynamics, it is a useful case to examine, because the data is unusually legible and the lessons appear to generalize.&lt;/p&gt;

</description>
      <category>italy</category>
      <category>creatoreconomy</category>
      <category>marketplaces</category>
      <category>retrospective</category>
    </item>
  </channel>
</rss>
