// ================================================================ // Tradatix Journal — NinjaTrader 8 Indicator // Registra automáticamente operaciones cerradas en tu Journal // // INSTALACIÓN: // 1. En NT8: Herramientas → Editor NinjaScript → Nueva clase // → Indicador → nombre: TradatixJournal → pega este código // 2. Compila (F5) // 3. Abre un gráfico de cualquier instrumento // 4. Indicadores → Añadir → Tradatix Journal → pega tu API Key // 5. Listo — captura TODOS los trades de TODAS las cuentas // abiertas en NT8, sin necesidad de habilitarlo como estrategia // // NOTA: Mantén el gráfico con el indicador abierto mientras operas. // ================================================================ #region Using declarations using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Net.Http; using System.Text; using System.Threading.Tasks; using NinjaTrader.Cbi; using NinjaTrader.NinjaScript; using NinjaTrader.NinjaScript.Indicators; #endregion namespace NinjaTrader.NinjaScript.Indicators { public class TradatixJournal : Indicator { private static readonly HttpClient Http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; // Estado de posición abierta por cuenta+instrumento private class OpenPos { public double AvgPrice; public double Qty; public double Commission; public bool IsLong; public DateTime Time; } // Clave: "cuenta|instrumento" private readonly Dictionary _positions = new Dictionary(); // ── Parámetros ──────────────────────────────────────────── [NinjaScriptProperty] [Display(Name = "API Key de Tradatix", Order = 1, GroupName = "Tradatix Journal")] public string ApiKey { get; set; } [NinjaScriptProperty] [Display(Name = "URL del endpoint", Order = 2, GroupName = "Tradatix Journal")] public string ApiUrl { get; set; } [NinjaScriptProperty] [Display(Name = "Debug (ver logs en Salida)", Order = 3, GroupName = "Tradatix Journal")] public bool DebugMode { get; set; } // ── Ciclo de vida ───────────────────────────────────────── protected override void OnStateChange() { if (State == State.SetDefaults) { Name = "Tradatix Journal"; Description = "Registra operaciones cerradas en el Journal de Tradatix."; ApiKey = ""; ApiUrl = "https://tradatix.com/app/api.php?action=journal_mt5"; DebugMode = false; IsOverlay = true; IsSuspendedWhileInactive = false; // seguir activo aunque el gráfico esté en segundo plano } else if (State == State.DataLoaded) { // Suscribirse a TODAS las cuentas abiertas en NT8 foreach (var account in Account.All) account.ExecutionUpdate += OnAccountExecution; if (string.IsNullOrWhiteSpace(ApiKey)) Print("⚠️ Tradatix Journal: configura tu API Key en los parámetros del indicador."); else Print($"✅ Tradatix Journal activo — monitorizando {Account.All.Count} cuenta(s)"); } else if (State == State.Terminated) { foreach (var account in Account.All) account.ExecutionUpdate -= OnAccountExecution; } } // ── Captura de ejecuciones ──────────────────────────────── private void OnAccountExecution(object sender, ExecutionEventArgs args) { var exec = args.Execution; if (exec?.Order == null || exec.Quantity <= 0) return; var account = sender as Account; string key = $"{account?.Name}|{exec.Instrument?.FullName}"; bool isBuy = exec.Order.OrderAction == OrderAction.Buy || exec.Order.OrderAction == OrderAction.BuyToCover; double qty = exec.Quantity; double price = exec.Price; double comm = exec.Commission; if (!_positions.TryGetValue(key, out var pos)) { // Nueva posición _positions[key] = new OpenPos { AvgPrice = price, Qty = qty, Commission = comm, IsLong = isBuy, Time = exec.Time }; if (DebugMode) Print($"Tradatix | Apertura {(isBuy ? "BUY" : "SELL")} {qty} {exec.Instrument?.FullName} @ {price}"); return; } bool isClosing = (isBuy && !pos.IsLong) || (!isBuy && pos.IsLong); if (isClosing) { double closeQty = Math.Min(qty, pos.Qty); double pv = exec.Instrument?.MasterInstrument?.PointValue ?? 1.0; double grossPnl = (pos.IsLong ? (price - pos.AvgPrice) : (pos.AvgPrice - price)) * closeQty * pv; double ratio = pos.Qty > 0 ? closeQty / pos.Qty : 1; double totalComm = pos.Commission * ratio + comm; double netPnl = grossPnl - totalComm; long ticket = Math.Abs(pos.Time.Ticks ^ (long)key.GetHashCode() ^ (long)(price * 1e5)); _ = SendAsync( symbol : exec.Instrument?.FullName ?? "", direction : pos.IsLong ? "buy" : "sell", lots : closeQty, openPrice : pos.AvgPrice, closePrice: price, openTime : pos.Time, closeTime : exec.Time, profit : netPnl, commission: totalComm, ticket : ticket, accountRef: account?.Name ?? "" ); pos.Qty -= closeQty; pos.Commission -= pos.Commission * ratio; if (pos.Qty <= 0.0001) _positions.Remove(key); } else { // Scale-in: actualizar precio medio double newQty = pos.Qty + qty; pos.AvgPrice = (pos.AvgPrice * pos.Qty + price * qty) / newQty; pos.Qty = newQty; pos.Commission += comm; if (DebugMode) Print($"Tradatix | Scale-in {qty} @ {price} — Total: {pos.Qty} @ {pos.AvgPrice:F5}"); } } // ── Envío HTTP ──────────────────────────────────────────── private async Task SendAsync(string symbol, string direction, double lots, double openPrice, double closePrice, DateTime openTime, DateTime closeTime, double profit, double commission, long ticket, string accountRef = "") { if (string.IsNullOrWhiteSpace(ApiKey)) return; try { var ic = System.Globalization.CultureInfo.InvariantCulture; string json = string.Format(ic, "{{\"symbol\":\"{0}\",\"direction\":\"{1}\",\"lots\":{2}," + "\"open_price\":{3:F5},\"close_price\":{4:F5}," + "\"open_time\":\"{5}\",\"close_time\":\"{6}\"," + "\"profit\":{7:F2},\"commission\":{8:F2},\"ticket\":{9}," + "\"source_platform\":\"nt8\",\"account_ref\":\"{10}\"}}", Escape(symbol), direction, lots, openPrice, closePrice, openTime.ToString("yyyy-MM-dd HH:mm:ss"), closeTime.ToString("yyyy-MM-dd HH:mm:ss"), profit, commission, ticket, Escape(accountRef)); using var req = new HttpRequestMessage(HttpMethod.Post, ApiUrl); req.Headers.Add("X-Journal-Key", ApiKey); req.Content = new StringContent(json, Encoding.UTF8, "application/json"); var resp = await Http.SendAsync(req).ConfigureAwait(false); string body = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); if (DebugMode) Print($"Tradatix | {symbol} {direction} {lots} | " + $"P&L: {profit:F2} | HTTP {(int)resp.StatusCode} | {body}"); if ((int)resp.StatusCode != 200) Print($"Tradatix Journal ERROR HTTP {(int)resp.StatusCode}: {body}"); } catch (Exception ex) { Print($"Tradatix Journal ERROR: {ex.Message}"); } } private static string Escape(string s) => (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\""); protected override void OnBarUpdate() { } } }