<?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: Arthur de Oliveira Torres</title>
    <description>The latest articles on DEV Community by Arthur de Oliveira Torres (@arthurtorres).</description>
    <link>https://dev.to/arthurtorres</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%2F4023210%2F216ad51b-89be-4bfb-885d-b1a9e517cf43.jpg</url>
      <title>DEV Community: Arthur de Oliveira Torres</title>
      <link>https://dev.to/arthurtorres</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arthurtorres"/>
    <language>en</language>
    <item>
      <title>One Bug Hit 11 Rows. Another Hit 36 Million.</title>
      <dc:creator>Arthur de Oliveira Torres</dc:creator>
      <pubDate>Wed, 05 Aug 2026 03:13:02 +0000</pubDate>
      <link>https://dev.to/arthurtorres/one-bug-hit-11-rows-another-hit-36-million-lc0</link>
      <guid>https://dev.to/arthurtorres/one-bug-hit-11-rows-another-hit-36-million-lc0</guid>
      <description>&lt;p&gt;I built a predictive maintenance model on 37,164,054 telemetry records from an iron ore mine. Six months of alarms from haul trucks and excavators. The goal was to predict "Don't Go" events, the ones that stop a machine mid-operation, hours before they happen.&lt;/p&gt;

&lt;p&gt;Before any of that, I had to deal with four data quality problems.&lt;/p&gt;

&lt;p&gt;Here is what makes them interesting. One of them touched &lt;strong&gt;11 rows&lt;/strong&gt;. Another touched &lt;strong&gt;36,104,611&lt;/strong&gt;. Both were silent. Neither threw an exception.&lt;/p&gt;

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

&lt;p&gt;The dataset came from Vale's Programa Desenvolver with quality problems deliberately inserted as part of the challenge. Three were documented in the challenge material. The fourth was not, and I only found it when I stopped working on a sample.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Rows fixed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Criticidade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UTF-8 encoding corruption&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Classe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Literal string &lt;code&gt;"NULL"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;36,104,611&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Valor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Literal string &lt;code&gt;"NULL"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;237,443&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Valor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Comma as decimal separator&lt;/td&gt;
&lt;td&gt;821,849&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Eleven rows out of thirty seven million
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Criticidade&lt;/code&gt; column holds the alarm severity label. Almost every row was fine. Eleven were not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;N??o Crítico
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Não Crítico&lt;/code&gt; had lost its accented characters somewhere in the export chain, replaced by question marks.&lt;/p&gt;

&lt;p&gt;Eleven rows. That is 0.00003% of the dataset. It would never show up in a &lt;code&gt;head()&lt;/code&gt;, a random sample, or a summary statistic. But group by that column and you silently get an extra category, splitting one class into two.&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;_CRITICIDADE_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N.{1,2}o Cr.{1,2}tico&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Criticidade&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Criticidade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_CRITICIDADE_PATTERN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Não Crítico&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex is loose on purpose. I did not want to assume the corruption was always exactly two question marks, because it was not consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Thirty six million rows of the word "NULL"
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Classe&lt;/code&gt; column should hold &lt;code&gt;Activate&lt;/code&gt; or &lt;code&gt;Inactive&lt;/code&gt;. Instead, 97% of the dataset held 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="s2"&gt;"NULL"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a null value. The four characters N, U, L, L, as text.&lt;/p&gt;

&lt;p&gt;Every null check I had written came back clean. &lt;code&gt;isna()&lt;/code&gt; saw a perfectly valid string. The column reported 100% populated while being almost entirely empty.&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Classe&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Classe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NULL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the one that worries me most, because it fails in the direction of looking healthy. A column that reports zero missing values does not get a second look.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A decimal separator that changed its mind
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Valor&lt;/code&gt; holds the numeric sensor reading behind each alarm. It arrived typed as a string, which was already a hint. Inside, 821,849 rows looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"43,7999992370605"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comma as decimal separator, Brazilian convention, in a column that also held dot-separated values.&lt;/p&gt;

&lt;p&gt;Cast the column to float and you get an exception. Cast with &lt;code&gt;errors="coerce"&lt;/code&gt; and you get NaN exactly where the values were, which then reads as missing data rather than as a bug you introduced.&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Valor&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Valor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&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;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. The one that only appeared at full scale
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Valor&lt;/code&gt; also contained the literal string &lt;code&gt;"NULL"&lt;/code&gt;. 237,443 rows of it.&lt;/p&gt;

&lt;p&gt;I did not find this one in the challenge documentation, and I did not find it while iterating on a sample. It surfaced the first time I ran the pipeline against all six months at once.&lt;/p&gt;

&lt;p&gt;That is the part worth sitting with. The sample was not small: it was large enough to feel representative and fast enough to iterate on. It still hid a problem affecting a quarter of a million rows, because 237,443 out of 37 million is 0.6%, and 0.6% is easy to miss when you are slicing the first N rows of one file.&lt;/p&gt;

&lt;p&gt;The fix is one line. Finding it was the work:&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;mask_valor_null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Valor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&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;NULL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mask_valor_null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Valor&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;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ordering matters here. This has to run before the comma replacement, or &lt;code&gt;"NULL"&lt;/code&gt; reaches &lt;code&gt;astype(float)&lt;/code&gt; and takes the whole cast down with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that made this tractable
&lt;/h2&gt;

&lt;p&gt;I stopped writing cleaning code first. Instead every fix reports what it touched, into a dataclass:&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="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QualityReport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;criticidade_fixed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;null_string_fixed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;decimal_fixed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;valor_null_fixed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;inicio_after_fim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;tag_mismatches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;duplicate_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;timestamp_gaps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things came out of that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The report is the diagnosis.&lt;/strong&gt; When I had to defend the choices in the final report, I had exact counts per problem, not a memory of having fixed something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cleaning becomes testable.&lt;/strong&gt; Every fix got a test asserting the count. Reruns are cheap and regressions are loud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has room for problems I had not found yet.&lt;/strong&gt; Fields like &lt;code&gt;inicio_after_fim&lt;/code&gt;, &lt;code&gt;tag_mismatches&lt;/code&gt; and &lt;code&gt;timestamp_gaps&lt;/code&gt; are checks on the operational log that were not part of the planted three. Building the structure to hold unknown problems is what let the fourth one surface as a number instead of as a crash three notebooks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;p&gt;None of these four throws an exception. A pipeline with no validation layer runs end to end, trains a model, prints a metric, and is wrong in a way no stack trace will ever mention.&lt;/p&gt;

&lt;p&gt;The spread is the lesson. Eleven rows and thirty six million rows are the same class of bug and need the same defense, because neither announces itself. At this volume you cannot eyeball anything, and your sample is not as representative as it feels.&lt;/p&gt;

&lt;p&gt;The validator is not overhead you add if there is time left. It is the thing standing between you and a confident wrong answer.&lt;/p&gt;




&lt;p&gt;Full case study, including SHAP explainability and what the model actually learned (which was not what I hoped): &lt;a href="https://torres-dev-ai.vercel.app/projects/vale-desenvolver-2026" rel="noopener noreferrer"&gt;https://torres-dev-ai.vercel.app/projects/vale-desenvolver-2026&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source code, tests and metrics: &lt;a href="https://github.com/tutorres/Vale_Desenvolver" rel="noopener noreferrer"&gt;https://github.com/tutorres/Vale_Desenvolver&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dataset is proprietary and is not redistributed.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>pandas</category>
      <category>datascience</category>
    </item>
    <item>
      <title>From Raw Prices to Real Signals: Financial Metrics for a Data Pipeline (Daily Return, MA, RSI, MACD, Volatility)</title>
      <dc:creator>Arthur de Oliveira Torres</dc:creator>
      <pubDate>Thu, 09 Jul 2026 20:29:43 +0000</pubDate>
      <link>https://dev.to/arthurtorres/from-raw-prices-to-real-signals-financial-metrics-for-a-data-pipeline-daily-return-ma-rsi-1m31</link>
      <guid>https://dev.to/arthurtorres/from-raw-prices-to-real-signals-financial-metrics-for-a-data-pipeline-daily-return-ma-rsi-1m31</guid>
      <description>&lt;p&gt;In this article I will share my understanding and use of some financial metrics used in my Financial Dashboard project. You can see it in this link and the full article by clicking here.&lt;/p&gt;

&lt;p&gt;The main point here is understanding the math used to transform data from the &lt;strong&gt;bronze layer&lt;/strong&gt; — where we have raw information such as date, open, high, low, close, and volume — and aggregate useful data to improve consumption and future use in the data pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  First metric: Daily Return
&lt;/h2&gt;

&lt;p&gt;Not a difficult one. It returns the percentage change between today's and yesterday's stock prices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Seeing day-to-day changes can be useful and later used in the gold layer for analysis.&lt;/p&gt;

&lt;p&gt;The math is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(today - yesterday) / yesterday
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;day 1: 150.0 → &lt;code&gt;NaN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;day 2: 152.3 → &lt;code&gt;(152.3 - 150.0) / 150.0 = +0.0153 (+1.53%)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;day 3: 149.8 → &lt;code&gt;(149.8 - 152.3) / 152.3 = -0.0164 (-1.64%)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple, right? In code, we use pandas:&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;close&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pct_change&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Second metric: Moving Averages (MA7, MA21, MA50)
&lt;/h2&gt;

&lt;p&gt;Moving average shows the average price over the last X days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Individually, they show the direction of the stock — not very powerful alone, but when analyzed together they can provide strong signals. Golden cross and death cross are the most relevant examples.&lt;/p&gt;

&lt;p&gt;The math is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MA(X) = (day_t + day_(t-1) + ... + day_(t-X+1)) / X
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see, for example, MA7 will only return a valid result after the 7th day.&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ma_7&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;close&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rolling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Third metric: RSI (Relative Strength Index)
&lt;/h2&gt;

&lt;p&gt;Probably the most complex in this list, so let's go step by step.&lt;/p&gt;

&lt;p&gt;RSI returns a number between 0 and 100:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;70+&lt;/strong&gt; → overbought&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;30-&lt;/strong&gt; → oversold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; It helps analyze market momentum. Sometimes stocks are not priced only by their real value — hype, news, or unexpected events can influence behavior.&lt;/p&gt;

&lt;p&gt;Let's dive into the math:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Period = 14 (default market value)&lt;/li&gt;
&lt;li&gt;Delta = difference between current day and previous day&lt;/li&gt;
&lt;li&gt;Gain = delta if positive, otherwise 0&lt;/li&gt;
&lt;li&gt;Loss = absolute delta if negative, otherwise 0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average Gain = exponential moving average (EWM) of gains&lt;/li&gt;
&lt;li&gt;Average Loss = exponential moving average (EWM) of losses&lt;/li&gt;
&lt;li&gt;RS = avg_gain / avg_loss&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  About EWM
&lt;/h3&gt;

&lt;p&gt;EWM (Exponential Weighted Mean) gives more weight to recent values. The weight decays exponentially over time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;most recent day: &lt;code&gt;weight = 1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;2 days ago: &lt;code&gt;weight = (1 - α)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;3 days ago: &lt;code&gt;weight = (1 - α)²&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If α is close to 1  →  fast decay (focus on recent data)
If α is close to 0  →  slow decay (longer memory)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the RSI calculation, we use &lt;code&gt;com=period-1&lt;/code&gt; in pandas, which relates to alpha as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;α = 1 / (1 + com) = 1 / 14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small alpha → slow decay → all 14 days matter, not just the most recent ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final RSI formula
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RSI = 100 - (100 / (1 + RS))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is interpreted as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RSI &amp;lt; 30  →  oversold&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RSI &amp;gt; 70  →  overbought&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_rsi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;gain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="o"&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;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="o"&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;avg_gain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ewm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_periods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;avg_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ewm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_periods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;avg_gain&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;avg_loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&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="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# Avoid division by zero
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Fourth indicator: MACD (Moving Average Convergence Divergence)
&lt;/h2&gt;

&lt;p&gt;MACD shows the momentum of the stock using EMA (Exponential Moving Average) to weight recent values more heavily.&lt;/p&gt;

&lt;p&gt;Unlike a simple moving average where all days have equal weight, EMA applies exponential decay:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EMA(t) = Price(t) × α + EMA(t-1) × (1 - α)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where alpha is defined as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;α = 2 / (span + 1)

span = 12  →  α ≈ 0.154  (more reactive, short-term)
span = 26  →  α ≈ 0.074  (smoother, long-term)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So EMA12 reacts faster to price changes, while EMA26 captures the longer trend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MACD = EMA12 - EMA26
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;MACD Signal line&lt;/strong&gt; is an EMA of the MACD itself (span=9), used to identify buy/sell triggers.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;MACD Histogram&lt;/strong&gt; is the difference between MACD and Signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MACD Histogram = MACD - Signal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Positive and going up&lt;/td&gt;
&lt;td&gt;Growing momentum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positive and going down&lt;/td&gt;
&lt;td&gt;High stock losing strength&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negative and going down&lt;/td&gt;
&lt;td&gt;Downward momentum accelerating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negative and rising&lt;/td&gt;
&lt;td&gt;Going down but losing strength&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Fifth metric: Volatility (21-day)
&lt;/h2&gt;

&lt;p&gt;Simple but useful — evaluates the standard deviation of daily returns over the last 21 days. Higher volatility means bigger price oscillations in the period. We use it to set risk/reward accuracy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Code (MACD + Volatility)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ema12&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ewm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adjust&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;ema26&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ewm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adjust&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;macd&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;ema12&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ema26&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;macd_signal&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;macd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ewm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adjust&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;macd_hist&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;macd&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;macd_signal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;volatility_21&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily_return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rolling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, we complete our silver layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We can create from raw data some really interesting and useful indicators to aggregate information and extract real value from it. Data without purpose is useless — we need to learn how to extract meaning from it.&lt;/p&gt;

&lt;p&gt;And that's the point of this article: not the math, not the code, but that we can get real-world insights from data. That's what makes this work worthwhile.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>finance</category>
      <category>pandas</category>
    </item>
  </channel>
</rss>
