Welcome to Boletin.info   Click to listen highlighted text! Welcome to Boletin.info
Taller manos a la obra: monitor OLED con ESP8266 para detectar cambios de tendencia (Gold, DAX, USDJPY, Nasdaq)
Taller manos a la obra con un NodeMCU ESP8266 y su OLED integrada: construimos “TrendFlip-4”, un mini monitor que consulta Gold, DAX, USDJPY y NDX cada 15 minutos, calcula EMA(9/21) y resalta con “!” los cambios de dirección en tiempo real.

Hay proyectos que valen por dos cosas: por lo que aprendes al construirlos y por lo útil que resulta el objeto terminado. Este taller propone exactamente eso: un monitor minimalista con ESP8266 + OLED 0.96" que vigila cuatro instrumentos (Gold, DAX, USDJPY y NDX / Nasdaq 100) y muestra una señal simple:

  • COMPRAR
  • VENDER
  • ESPERAR

La filosofía es deliberadamente práctica: un panel “de reojo”. Si todo sigue igual, no haces nada. Solo te importa cuando aparece lo verdaderamente relevante:

Cambié de VENDER a COMPRAR (o viceversa) y el equipo te lo marca como cambio reciente.


1) Qué vamos a construir (resultado final)

En la OLED verás 4 líneas, una por instrumento:

  • GOLD COMPRAR!
  • DAX ESPERAR
  • USDJPY VENDER
  • NDX COMPRAR

Donde:

  • COMPRAR/VENDER/ESPERAR es la señal actual.
  • El ! se muestra cuando hubo un cambio de dirección detectado recientemente.

Ese ! es el “disparador visual”: si aparece, vale la pena revisar con más detalle en tu plataforma (no en el microcontrolador). Si no aparece, el monitor está cumpliendo su función de no distraerte.


2) Enfoque del taller: sin backend, solo Web

Todo sucede dentro del ESP8266:

  1. Conecta al Wi-Fi
  2. Consulta un API web de mercado (HTTPS)
  3. Calcula la señal localmente
  4. Dibuja el tablero en la OLED

Sin servidor intermedio, sin base de datos, sin infraestructura adicional.

Para los ejemplos de este taller uso Twelve Data, porque unifica commodities, forex e índices bajo la misma API y documentación. (Twelve Data)


3) Señal de tendencia: COMPRAR / VENDER / ESPERAR (simple y estable)

Para que este panel sea útil, la señal debe ser:

  • fácil de interpretar,
  • relativamente estable (sin parpadeo),
  • coherente con un “cambio en 15 minutos”.

Por eso trabajamos con velas de 15 minutos y dos medias móviles exponenciales:

  • EMA rápida (9)
  • EMA lenta (21)

Regla (tri-estado):

  • COMPRAR si EMA9 > EMA21 y la separación supera un umbral mínimo
  • VENDER si EMA9 < EMA21 y la separación supera un umbral mínimo
  • ESPERAR si están muy cerca (zona neutra)

Esto evita que la pantalla cambie de opinión por micro-ruido.


4) “Cambio de dirección” y el icono !

Guardamos el estado anterior de cada instrumento y lo comparamos con el nuevo:

  • VENDER → COMPRAR = reversa alcista
  • COMPRAR → VENDER = reversa bajista

Cuando ocurre, mostramos ! durante un tiempo fijo.

Nota práctica: en horario activo, el dispositivo consulta cada 15 minutos, por lo que ! se interpreta como “cambio detectado en el último ciclo de 15m”. Fuera de horario, consultamos menos para ahorrar cuota; el ! sigue siendo útil, pero se detecta con menos frecuencia.


5) Control de cuota (plan gratuito): frecuencia inteligente

Para mantener el proyecto dentro de márgenes gratuitos, aplicamos una regla simple:

  • 04:00–16:00 → consultar cada 15 minutos
  • 16:00–04:00 → consultar cada 45 minutos (un tercio)

Con 4 instrumentos, esto suele quedar holgado incluso en planes gratuitos. (Además, Twelve Data soporta “bulk requests”, aunque cada símbolo consume su crédito; sirve para simplificar llamadas, no necesariamente para “gastar menos”.) (support.twelvedata.com)


Preparación en Windows (paso a paso)

Si hace tiempo no trabajas con ESP8266, esta sección te evita perder tiempo.

1) Instalar Arduino IDE 2.x

  1. Descarga e instala Arduino IDE 2.x. Recomiendo hacerlo del sitio oficial, seleccionar el sistema operativo y oprimir e boton de download. https://www.arduino.cc/en/software/
  2. Ábrelo una vez para que cree su configuración.

2) Conectar la placa y verificar el puerto COM (CH340)

  1. Conecta el NodeMCU por USB.
  2. Abre Administrador de dispositivosPorts (COM & LPT).
  3. Debe aparecer algo como USB-SERIAL CH340 (COMx).

Como se muestra en mi imagen, en mi computadora quedó en COM3

Si no aparece o hay advertencia:

  • instala el driver CH340 (muchas veces Windows 10/11 lo instala solo, pero no siempre).

3) Agregar soporte de placas ESP8266

  1. Arduino IDE → File → Preferences
  2. En Additional Boards Manager URLs pega:http://arduino.esp8266.com/stable/package_esp8266com_index.json
  3. Tools → Board → Boards Manager
  4. Busca ESP8266 e instala “ESP8266 by ESP8266 Community”
  5. Selecciona la placa:
    • Tools → Board → NodeMCU 1.0 (ESP-12E Module)

Asi se ve luego de agregar http://arduino.esp8266.com/stable/package_esp8266com_index.json

Y luego de aplicar esta opcion y dirigirme a Tools → Board → Boards Manager me aparece la opcion que me permite instalar el paquete adecuado....

4) Configuración típica (recomendada)

En Tools:

  • Upload Speed: 115200 (si falla, baja a 57600)
  • CPU Frequency: 80 MHz
  • Flash Size: el típico de tu placa (muchas son 4MB)

5) Instalar librerías (Library Manager)

  • Adafruit GFX Library
  • Adafruit SSD1306
  • ArduinoJson (v6)

Es usual que al solicitar una librería, también sea necesario instalar dependencias. En tal caso se acepta que se instalen las dependencias. Por ejemplo al instqalar Adafruit SSD1306 tambien se agregan otras como se ve en la siguiente imagen.


Pantalla OLED: cableado y verificación

Cableado típico I2C (NodeMCU)

  • SDA → D2 (GPIO4)
  • SCL → D1 (GPIO5)
  • VCC → 3.3V
  • GND → GND

La dirección I2C más común del SSD1306 es 0x3C.

En las pruebas de mi equipo “NodeMCU ESP8266 + OLED integrada”, la pantalla no siempre usa los pines I2C típicos del NodeMCU (D2/D1). En mi placa, la OLED funciona con:

  • SDA = D6
  • SCL = D5
  • Dirección I2C: 0x3C
  • Controlador: SSD1306 (128×64)

Nota visual: muchas OLED 0.96” son bicolor (franja superior amarilla y resto azul). No es un “color por software”: la pantalla es monocromática, y el “amarillo/azul” depende de la zona física del display.


Apéndice A — Fuente de datos (Twelve Data) y símbolos

Este taller usa el endpoint time_series (velas) porque es robusto y no depende de “scraping”. (Twelve Data)

Símbolos confirmados (Twelve Data)

Para DAX y NDX (Nasdaq 100)

En índices, el nombre exacto puede variar según el proveedor. En lugar de adivinarlo, lo resolvemos “hands-on” desde el navegador con symbol_search (sin programar nada afuera):

Ejemplos (pega en el navegador, reemplazando TU_API_KEY):

  • Buscar DAX:
https://api.twelvedata.com/symbol_search?symbol=DAX&apikey=TU_API_KEY
  • Buscar NDX (Nasdaq 100):
https://api.twelvedata.com/symbol_search?symbol=NDX&apikey=TU_API_KEY

Escoge el resultado que corresponda a índice (no CFD ni derivados) y copia el campo symbol tal cual para el sketch.

Prueba rápida del endpoint (en navegador)

(Con símbolo ya confirmado)

https://api.twelvedata.com/time_series?symbol=USD%2FJPY&interval=15min&outputsize=60&apikey=TU_API_KEY

Apéndice B — “Hello OLED” (prueba mínima)

Objetivo

Validar tres cosas antes del monitor de trading:

  1. Que la placa ejecuta tu firmware (Serial “Tick…”).
  2. Que la OLED responde (texto visible).
  3. Que el bus I2C y pines correctos están definidos (en este modelo: D6/D5).

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

unsigned long t0 = 0;
bool ledState = false;

void setup() {
  // LED (en ESP8266 suele ser activo en LOW)
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);

  // Serial para ver “vida” del programa
  Serial.begin(115200);
  delay(200);
  Serial.println();
  Serial.println("BOOT: sketch nuevo iniciando...");

  // IMPORTANTE: en este modelo con OLED integrada
  // SDA = D6, SCL = D5  asi es en otros modelos...
  Wire.begin(D6, D5);

  // OLED SSD1306 normalmente en 0x3C
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("ERROR: OLED no inicia (direccion/pines incorrectos).");
    while (true) { delay(1000); }
  }

  // Dibujar en pantalla
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  // Nota: en OLED “bicolor” la franja superior se ve amarilla físicamente
  display.setCursor(0, 0);
  display.println("HOLA UNIVERSO!");
  display.println("Escribe tu nombre");
  display.println();
  display.println("SKETCH de prueba!");
  display.println("Se juega con estilos");
  display.println();

  // Resaltado (texto negro sobre fondo blanco)
  display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
  display.println("   COLOR INVERTIDO   ");

  // Volver a normal
  display.setTextColor(SSD1306_WHITE);
  display.println("COLOR NORMAL");

  display.display();

  Serial.println("OLED: OK (deberias ver HOLA UNIVERSO).");
}

void loop() {
  // “Heartbeat” por Serial + parpadeo LED
  if (millis() - t0 >= 1000) {
    t0 = millis();

    ledState = !ledState;
    digitalWrite(LED_BUILTIN, ledState ? LOW : HIGH);

    Serial.println("Tick: estoy corriendo...");
  }
}

En micaso, mi arduino se ve asi... con esta prueba.

Apéndice C — Sketch completo “TrendFlip-4” (un solo archivo)

Este sketch hace:

  • NTP (hora) para decidir horario activo 04:00–16:00 (Costa Rica UTC-6)
  • Consulta velas 15m (HTTPS)
  • Calcula EMA(9) y EMA(21)
  • Señal tri-estado + detección de reversa
  • ! durante 15 minutos cuando detecta cambio

Importante: para simplificar HTTPS, se usa setInsecure() (no recomendado para proyectos sensibles, pero suficiente para este laboratorio).

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>

#include <Wire.h>
#include <time.h>

#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ArduinoJson.h>

// ===== OLED =====
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// ====== CONFIG WIFI / API ======
const char* WIFI_SSID = "TU_WIFI";
const char* WIFI_PASS = "TU_CLAVE";

const char* TD_API_KEY = "TU_API_KEY";
const char* TD_HOST    = "https://api.twelvedata.com";

// ====== DATA / INDICADORES ======
const char* INTERVAL = "15min";
const int OUTPUTSIZE = 60;    // velas a pedir (cierra bien para EMA 21)
const int EMA_FAST = 9;
const int EMA_SLOW = 21;

// Frecuencia: 04:00–16:00 cada 15 min; fuera cada 45 min
const unsigned long REFRESH_ACTIVE_MS = 15UL * 60UL * 1000UL;
const unsigned long REFRESH_IDLE_MS   = 45UL * 60UL * 1000UL;

// Icono de cambio reciente (15 min)
const unsigned long HIGHLIGHT_MS = 15UL * 60UL * 1000UL;

// Zona horaria Costa Rica: UTC-6
const long TZ_OFFSET_SEC = -6L * 3600L;

// Umbral de neutralidad (en %). Si EMA muy cerca => “ESPERAR”
const double NEUTRAL_PCT = 0.03; // 0.03%

// ====== INSTRUMENTOS ======
// Para DAX y NDX: confirma con symbol_search y pega el symbol exacto del proveedor.
struct Instrument {
  const char* label;      // Texto OLED
  const char* symbol;     // Símbolo API
  int lastSignal;         // -1 (VENDER), 0 (ESPERAR), +1 (COMPRAR)
  unsigned long lastFlipMillis;
};

Instrument instruments[] = {
  {"GOLD ",  "XAU/USD",  0, 0},
  {"DAX  ",  "DAX",      0, 0},   // <-- reemplazar por el symbol exacto
  {"USDJPY", "USD/JPY",  0, 0},
  {"NDX  ",  "NDX",      0, 0}    // <-- reemplazar por el symbol exacto Nasdaq 100
};
const int N = sizeof(instruments) / sizeof(instruments[0]);

// ====== UTIL ======
String urlEncode(const String& s) {
  String out;
  for (size_t i = 0; i < s.length(); i++) {
    char c = s[i];
    if (c == ' ') out += "%20";
    else if (c == '/') out += "%2F";
    else if (c == ':') out += "%3A";
    else out += c;
  }
  return out;
}

const char* signalText(int s) {
  if (s > 0) return "COMPRAR";
  if (s < 0) return "VENDER ";
  return "ESPERAR";
}

double computeEMA(const double* prices, int count, int period) {
  if (count <= 0) return 0;
  const double alpha = 2.0 / (period + 1.0);
  double ema = prices[0]; // seed con el dato más antiguo
  for (int i = 1; i < count; i++) {
    ema = alpha * prices[i] + (1.0 - alpha) * ema;
  }
  return ema;
}

int computeSignalFromCloses(const double* closes, int count) {
  double emaFast = computeEMA(closes, count, EMA_FAST);
  double emaSlow = computeEMA(closes, count, EMA_SLOW);
  double lastClose = closes[count - 1];

  double diffPct = ((emaFast - emaSlow) / lastClose) * 100.0;

  // Zona neutra
  if (fabs(diffPct) < NEUTRAL_PCT) return 0;

  return (diffPct > 0) ? +1 : -1;
}

bool fetchSignal(const char* symbol, int &outSignal) {
  WiFiClientSecure client;
  client.setInsecure(); // laboratorio: simplifica TLS

  HTTPClient https;
  String sym = urlEncode(String(symbol));

  String url = String(TD_HOST)
    + "/time_series?symbol=" + sym
    + "&interval=" + INTERVAL
    + "&outputsize=" + String(OUTPUTSIZE)
    + "&apikey=" + TD_API_KEY;

  if (!https.begin(client, url)) return false;

  int code = https.GET();
  if (code != 200) { https.end(); return false; }

  // Filtramos solo closes (ahorra RAM)
  StaticJsonDocument<128> filter;
  filter["values"][0]["close"] = true;

  DynamicJsonDocument doc(4096);
  DeserializationError err = deserializeJson(
    doc,
    https.getStream(),
    DeserializationOption::Filter(filter)
  );

  https.end();
  if (err) return false;

  JsonArray values = doc["values"].as<JsonArray>();
  int count = values.size();
  if (count < 25) return false;

  // Twelve Data devuelve newest-first. Pasamos a oldest->newest.
  static double closes[OUTPUTSIZE];
  int idx = 0;
  for (int i = count - 1; i >= 0 && idx < OUTPUTSIZE; i--) {
    const char* c = values[i]["close"];
    if (!c) continue;
    closes[idx++] = atof(c);
  }
  if (idx < 25) return false;

  outSignal = computeSignalFromCloses(closes, idx);
  return true;
}

bool isActiveWindow() {
  time_t now = time(nullptr);
  if (now < 100000) return true; // si NTP aún no está, arranca “activo”
  tm* t = localtime(&now);
  int h = t->tm_hour;
  return (h >= 4 && h < 16);
}

String timeHHMM() {
  time_t now = time(nullptr);
  if (now < 100000) return "--:--";
  tm* t = localtime(&now);
  char buf[6];
  snprintf(buf, sizeof(buf), "%02d:%02d", t->tm_hour, t->tm_min);
  return String(buf);
}

void drawUI() {
  display.clearDisplay();
  display.setTextSize(1);

  // Header
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.print(timeHHMM());

  display.setCursor(64, 0);
  display.print("RSSI ");
  display.print(WiFi.RSSI());

  const int y0 = 14;
  const int rowH = 12;

  for (int i = 0; i < N; i++) {
    bool highlight = (millis() - instruments[i].lastFlipMillis) < HIGHLIGHT_MS;
    int y = y0 + i * rowH;

    if (highlight) {
      // Resaltado de fila: fondo blanco + texto negro
      display.fillRect(0, y - 1, 128, rowH, SSD1306_WHITE);
      display.setTextColor(SSD1306_BLACK);
    } else {
      display.setTextColor(SSD1306_WHITE);
    }

    display.setCursor(0, y);
    display.print(instruments[i].label);
    display.print(" ");
    display.print(signalText(instruments[i].lastSignal));
    if (highlight) display.print("!");

    // Volver a color normal por seguridad
    display.setTextColor(SSD1306_WHITE);
  }

  display.display();
}

// ====== MAIN ======
unsigned long lastRefresh = 0;

void showBootScreen(const char* line2) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("TrendFlip-4");
  display.println(line2);
  display.display();
}

void setup() {
  // I2C para tu modelo (OLED integrada):
  // SDA = D6, SCL = D5
  Wire.begin(D6, D5);

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  showBootScreen("Conectando WiFi...");

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED) {
    delay(250);
    if (millis() - start > 20000) break; // evita loop infinito
  }

  if (WiFi.status() == WL_CONNECTED) {
    showBootScreen("WiFi OK. Iniciando NTP...");
  } else {
    showBootScreen("WiFi FAIL (seguimos)...");
  }

  // NTP
  configTime(TZ_OFFSET_SEC, 0, "pool.ntp.org", "time.nist.gov");
  delay(500);

  lastRefresh = 0; // fuerza primera lectura
}

void loop() {
  unsigned long intervalMs = isActiveWindow() ? REFRESH_ACTIVE_MS : REFRESH_IDLE_MS;

  if (millis() - lastRefresh >= intervalMs) {
    lastRefresh = millis();

    for (int i = 0; i < N; i++) {
      int newSignal = instruments[i].lastSignal;
      bool ok = fetchSignal(instruments[i].symbol, newSignal);

      if (ok) {
        int old = instruments[i].lastSignal;

        // Cambio real de dirección: VENDER <-> COMPRAR
        // (Ignoramos ESPERAR como “puente”)
        bool flipUp   = (old < 0 && newSignal > 0);
        bool flipDown = (old > 0 && newSignal < 0);

        if (flipUp || flipDown) {
          instruments[i].lastFlipMillis = millis();
        }

        instruments[i].lastSignal = newSignal;
      }

      delay(200); // separa requests (amable con el API)
    }
  }

  drawUI();
  delay(250);
}

Cierre

Con este taller terminas con un “semáforo” real: cuatro instrumentos, una señal clara, y un marcador explícito de cambio de dirección. El valor está en lo práctico: conectividad, APIs, JSON, cálculo de indicadores y una interfaz OLED limpia, todo en un proyecto único.

Si quieres, el siguiente ajuste fino que más impacto tiene es evitar falsos flips: exigir que el cambio se confirme en 2 lecturas consecutivas antes de activar el !.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Click to listen highlighted text!