//+------------------------------------------------------------------+ //| AI Logic.mqh | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" //--- Include guard #ifndef AI_LOGIC_MQH #define AI_LOGIC_MQH //--- Include required libraries #include #include "AI JSON FILE.mqh" #include "AI Canvas State.mqh" #include "AI Canvas Render.mqh" //+------------------------------------------------------------------+ //| Forward Extern Declarations | //+------------------------------------------------------------------+ #ifndef AI_COMPILED_FROM_MAIN extern string OpenAI_Model; // OpenAI model identifier extern string OpenAI_Endpoint; // OpenAI API endpoint URL extern int MaxResponseLength; // Maximum response token length extern string LogFileName; // Log file name extern int MaxChartBars; // Maximum chart bars to send extern bool DeleteLogsOnChatDelete; // Delete logs when chat is deleted extern bool AutoTrade; // Enable auto-trade execution extern double LotSize; // Trade lot size extern string OpenAI_API_Key; // OpenAI API key #endif //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ CTrade g_ai_trade; // Trade object for order execution int g_ai_logFileHandle = INVALID_HANDLE; // Log file handle string g_ai_chatsFileName = "ChatGPT_Chats.txt"; // Chats persistence file name string AI_PREP_BASE = "AI: Preparing the Request"; // Pre-animation base text string AI_LOADING_PLACEHOLDER= "AI: Thinking..."; // Loading placeholder text int AI_PRE_ANIM_CYCLES = 6; // Pre-animation cycle count ulong g_ai_startTimeMs = 0; // Request start time tracker //+------------------------------------------------------------------+ //| Convert timeframe enum to string | //+------------------------------------------------------------------+ string AiPeriodToString(ENUM_TIMEFRAMES period) { //--- Map standard timeframes to short labels switch(period) { case PERIOD_M1: return "M1"; case PERIOD_M5: return "M5"; case PERIOD_M15: return "M15"; case PERIOD_M30: return "M30"; case PERIOD_H1: return "H1"; case PERIOD_H4: return "H4"; case PERIOD_D1: return "D1"; case PERIOD_W1: return "W1"; case PERIOD_MN1: return "MN1"; default: return IntegerToString(period); } } //+------------------------------------------------------------------+ //| Build chart data string for AI prompt | //+------------------------------------------------------------------+ string AiGetChartDataString() { //--- Get chart symbol and timeframe info string symbol = Symbol(); ENUM_TIMEFRAMES tf = (ENUM_TIMEFRAMES)_Period; string timeframe = AiPeriodToString(tf); //--- Get visible bars on chart long visibleBarsLong = ChartGetInteger(0, CHART_VISIBLE_BARS); int visibleBars = (int)visibleBarsLong; //--- Copy recent rates MqlRates rates[]; int copied = CopyRates(symbol, tf, 1, MaxChartBars, rates); //--- Bail on copy failure if(copied != MaxChartBars) { Print("Failed to copy rates: ", GetLastError()); return ""; } //--- Order newest first ArraySetAsSeries(rates, true); //--- Build header line string data = "Chart Details: Symbol=" + symbol + ", Timeframe=" + timeframe + ", Visible Bars=" + IntegerToString(visibleBars) + "\n"; data += "Recent Bars Data (Bar 1 is latest COMPLETE bar):\n"; //--- Append OHLC data per bar for(int i = 0; i < copied; i++) { data += "Bar " + IntegerToString(i + 1) + ": Date=" + TimeToString(rates[i].time, TIME_DATE | TIME_MINUTES) + ", Open=" + DoubleToString(rates[i].open, _Digits) + ", High=" + DoubleToString(rates[i].high, _Digits) + ", Low=" + DoubleToString(rates[i].low, _Digits) + ", Close=" + DoubleToString(rates[i].close, _Digits) + ", Volume="+ IntegerToString((int)rates[i].tick_volume) + "\n"; } return data; } //+------------------------------------------------------------------+ //| Escape string for JSON encoding | //+------------------------------------------------------------------+ string AiJsonEscape(string value) { //--- Replace JSON-reserved characters StringReplace(value, "\\", "\\\\"); StringReplace(value, "\"", "\\\""); StringReplace(value, "\n", "\\n"); StringReplace(value, "\r", "\\r"); StringReplace(value, "\t", "\\t"); StringReplace(value, "\f", "\\f"); //--- Escape remaining control characters via unicode hex for(int i = 0; i < StringLen(value); i++) { ushort charCode = StringGetCharacter(value, i); if(charCode < 32 || charCode == 127) { string hex = StringFormat("\\u%04x", charCode); string before = StringSubstr(value, 0, i); string after = StringSubstr(value, i + 1); value = before + hex + after; i += 5; } } return value; } //+------------------------------------------------------------------+ //| Build OpenAI messages array from conversation history | //+------------------------------------------------------------------+ string AiBuildMessagesFromHistory(string newPrompt) { //--- Split history into lines string lines[]; int numLines = StringSplit(g_ai_conversationHistory, '\n', lines); //--- Initialize accumulator and current message state string messages = "["; string currentRole = ""; string currentContent = ""; //--- Walk lines and group into role-tagged messages for(int i = 0; i < numLines; i++) { string line = lines[i]; string trimmed = line; StringTrimLeft(trimmed); StringTrimRight(trimmed); //--- Skip blank and timestamp lines if(StringLen(trimmed) == 0 || AiIsTimestamp(trimmed)) continue; //--- Detect new user block if(StringFind(trimmed, "You: ") == 0) { if(currentRole != "") { string roleJson = (currentRole == "User") ? "user" : "assistant"; messages += "{\"role\":\"" + roleJson + "\",\"content\":\"" + AiJsonEscape(currentContent) + "\"},"; } currentRole = "User"; currentContent = StringSubstr(line, StringFind(line, "You: ") + 5); } //--- Detect new AI block else if(StringFind(trimmed, "AI: ") == 0) { if(currentRole != "") { string roleJson = (currentRole == "User") ? "user" : "assistant"; messages += "{\"role\":\"" + roleJson + "\",\"content\":\"" + AiJsonEscape(currentContent) + "\"},"; } currentRole = "AI"; currentContent = StringSubstr(line, StringFind(line, "AI: ") + 4); } //--- Continuation line of current block else if(currentRole != "") { currentContent += "\n" + line; } } //--- Flush trailing block if(currentRole != "") { string roleJson = (currentRole == "User") ? "user" : "assistant"; messages += "{\"role\":\"" + roleJson + "\",\"content\":\"" + AiJsonEscape(currentContent) + "\"},"; } //--- Append the new user prompt and close array messages += "{\"role\":\"user\",\"content\":\"" + AiJsonEscape(newPrompt) + "\"}]"; return messages; } //+------------------------------------------------------------------+ //| Send request to ChatGPT and return response | //+------------------------------------------------------------------+ string AiGetChatGPTResponse(string prompt, double temperature = 1.0) { //--- Build messages JSON from history plus new prompt string messages = AiBuildMessagesFromHistory(prompt); Print("Messages JSON: ", messages); if(g_ai_logFileHandle != INVALID_HANDLE) FileWrite(g_ai_logFileHandle, "Messages JSON: " + messages); //--- Build request body string requestData = "{\"model\":\"" + OpenAI_Model + "\",\"messages\":" + messages + ",\"max_tokens\":" + IntegerToString(MaxResponseLength) + ",\"temperature\":" + DoubleToString(temperature, 2) + "}"; Print("Request data (temp=", DoubleToString(temperature, 2), "): ", requestData); if(g_ai_logFileHandle != INVALID_HANDLE) FileWrite(g_ai_logFileHandle, "Request data (temp=" + DoubleToString(temperature, 2) + "): " + requestData); //--- Convert request to char array char postData[]; int dataLen = StringToCharArray(requestData, postData, 0, WHOLE_ARRAY, CP_UTF8); ArrayResize(postData, dataLen - 1); //--- Build HTTP headers string headers = "Authorization: Bearer " + OpenAI_API_Key + "\r\n" + "Content-Type: application/json; charset=UTF-8\r\n" + "Content-Length: " + IntegerToString(dataLen - 1) + "\r\n\r\n"; //--- Issue web request char result[]; string resultHeaders; int res = WebRequest("POST", OpenAI_Endpoint, headers, 10000, postData, result, resultHeaders); //--- Handle HTTP error if(res != 200) { string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); string errMsg = "API request failed: HTTP Code " + IntegerToString(res) + ", Error: " + IntegerToString(GetLastError()) + ", Response: " + response; Print(errMsg); return errMsg; } //--- Decode response body string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); Print("API response: ", response); if(g_ai_logFileHandle != INVALID_HANDLE) FileWrite(g_ai_logFileHandle, "API response: " + response); //--- Parse JSON response JsonValue jsonObject; int index = 0; char charArray[]; int arrayLength = StringToCharArray(response, charArray, 0, WHOLE_ARRAY, CP_UTF8); if(!jsonObject.DeserializeFromArray(charArray, arrayLength, index)) { string errMsg = "Error: Failed to parse API response JSON: " + response; Print(errMsg); return errMsg; } //--- Surface API error if present JsonValue *err = jsonObject.FindChildByKey("error"); if(err != NULL) { string errMsg = "API Error: " + err["message"].ToString(); Print(errMsg); return errMsg; } //--- Extract message content string content = jsonObject["choices"][0]["message"]["content"].ToString(); if(StringLen(content) > 0) { StringReplace(content, "\\n", "\n"); StringTrimLeft(content); StringTrimRight(content); return content; } //--- Fallback error string errMsg = "Error: No content in API response: " + response; Print(errMsg); return errMsg; } //+------------------------------------------------------------------+ //| Save chats to encrypted compressed file | //+------------------------------------------------------------------+ bool AiSaveChats() { //--- Build JSON array from chats JsonValue jsonArr; jsonArr.m_type = JsonArray; for(int i = 0; i < ArraySize(g_ai_chats); i++) { JsonValue obj; obj.m_type = JsonObject; obj["id"] = g_ai_chats[i].id; obj["title"] = g_ai_chats[i].title; obj["history"] = g_ai_chats[i].history; jsonArr.AddChild(obj); } //--- Serialize to string string jsonStr = jsonArr.SerializeToString(); uchar data[]; StringToCharArray(jsonStr, data); //--- Compress with ZIP uchar empty_key[]; uchar zipped[]; int res_zip = CryptEncode(CRYPT_ARCH_ZIP, data, empty_key, zipped); if(res_zip <= 0) { Print("Failed to compress chats: ", GetLastError()); return false; } //--- Derive AES key from API key SHA-256 hash uchar key[32]; uchar api_bytes[]; StringToCharArray(OpenAI_API_Key, api_bytes); uchar hash[]; CryptEncode(CRYPT_HASH_SHA256, api_bytes, empty_key, hash); ArrayCopy(key, hash, 0, 0, 32); //--- Encrypt with AES256 uchar encoded[]; int res_enc = CryptEncode(CRYPT_AES256, zipped, key, encoded); if(res_enc <= 0) { Print("Failed to encrypt chats: ", GetLastError()); return false; } //--- Write to disk int handle = FileOpen(g_ai_chatsFileName, FILE_WRITE | FILE_BIN); if(handle == INVALID_HANDLE) { Print("Failed to save chats: ", GetLastError()); return false; } FileWriteArray(handle, encoded, 0, res_enc); FileClose(handle); return true; } //--- Forward declarations for inter-dependent functions void AiCreateNewChat(); bool AiUpdateCurrentHistory(); //+------------------------------------------------------------------+ //| Load chats from encrypted compressed file | //+------------------------------------------------------------------+ void AiLoadChats() { //--- Create new chat if file missing if(!FileIsExist(g_ai_chatsFileName)) { AiCreateNewChat(); return; } //--- Open file for binary read int handle = FileOpen(g_ai_chatsFileName, FILE_READ | FILE_BIN); if(handle == INVALID_HANDLE) { Print("Failed to load chats: ", GetLastError()); AiCreateNewChat(); return; } //--- Read encrypted bytes int file_size = (int)FileSize(handle); uchar encoded_file[]; ArrayResize(encoded_file, file_size); FileReadArray(handle, encoded_file, 0, file_size); FileClose(handle); //--- Derive AES key from API key uchar empty_key[]; uchar key[32]; uchar api_bytes[]; StringToCharArray(OpenAI_API_Key, api_bytes); uchar hash[]; CryptEncode(CRYPT_HASH_SHA256, api_bytes, empty_key, hash); ArrayCopy(key, hash, 0, 0, 32); //--- Decrypt AES uchar decoded_aes[]; int res_dec = CryptDecode(CRYPT_AES256, encoded_file, key, decoded_aes); if(res_dec <= 0) { Print("Failed to decrypt chats: ", GetLastError()); AiCreateNewChat(); return; } //--- Decompress ZIP uchar decoded_zip[]; int res_zip = CryptDecode(CRYPT_ARCH_ZIP, decoded_aes, empty_key, decoded_zip); if(res_zip <= 0) { Print("Failed to decompress chats: ", GetLastError()); AiCreateNewChat(); return; } //--- Parse JSON string jsonStr = CharArrayToString(decoded_zip); char charArray[]; int len = StringToCharArray(jsonStr, charArray, 0, WHOLE_ARRAY, CP_UTF8); JsonValue json; int index = 0; if(!json.DeserializeFromArray(charArray, len, index)) { Print("Failed to parse chats JSON"); AiCreateNewChat(); return; } if(json.m_type != JsonArray) { Print("Chats JSON not an array"); AiCreateNewChat(); return; } //--- Populate chats array int size = ArraySize(json.m_children); ArrayResize(g_ai_chats, size); for(int i = 0; i < size; i++) { JsonValue obj = json.m_children[i]; g_ai_chats[i].id = (int)obj["id"].ToInteger(); g_ai_chats[i].title = obj["title"].ToString(); g_ai_chats[i].history = obj["history"].ToString(); } //--- Activate last chat or create fresh if(size > 0) { g_ai_currentChatId = g_ai_chats[size - 1].id; g_ai_currentTitle = g_ai_chats[size - 1].title; g_ai_conversationHistory = g_ai_chats[size - 1].history; } else { AiCreateNewChat(); } } //+------------------------------------------------------------------+ //| Create new empty chat | //+------------------------------------------------------------------+ void AiCreateNewChat() { //--- Compute next chat id int max_id = 0; for(int i = 0; i < ArraySize(g_ai_chats); i++) max_id = MathMax(max_id, g_ai_chats[i].id); int new_id = max_id + 1; //--- Append new chat record int size = ArraySize(g_ai_chats); ArrayResize(g_ai_chats, size + 1); g_ai_chats[size].id = new_id; g_ai_chats[size].title = "Chat " + IntegerToString(new_id); g_ai_chats[size].history = ""; //--- Set as active g_ai_currentChatId = new_id; g_ai_currentTitle = g_ai_chats[size].title; g_ai_conversationHistory = ""; g_ai_currentPrompt = ""; //--- Persist and refresh AiSaveChats(); g_ai_editor.SetText(""); Ai_RenderAll(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Update current chat record in chats array | //+------------------------------------------------------------------+ bool AiUpdateCurrentHistory() { //--- Find current chat index int idx = AiGetChatIndex(g_ai_currentChatId); bool savedOk = true; //--- Update record and save if(idx >= 0) { g_ai_chats[idx].history = g_ai_conversationHistory; g_ai_chats[idx].title = g_ai_currentTitle; savedOk = AiSaveChats(); } //--- Pin chat scroll to bottom for new content g_ai_chatScrollPin = true; return savedOk; } //+------------------------------------------------------------------+ //| Reopen log file handle in append mode | //+------------------------------------------------------------------+ void AiReopenLogHandle() { //--- Open log file for read/write g_ai_logFileHandle = FileOpen(LogFileName, FILE_READ | FILE_WRITE | FILE_TXT); if(g_ai_logFileHandle != INVALID_HANDLE) FileSeek(g_ai_logFileHandle, 0, SEEK_END); else Print("Failed to reopen log: ", GetLastError()); } //+------------------------------------------------------------------+ //| Clear log entries belonging to a deleted chat | //+------------------------------------------------------------------+ void AiClearLogsForChat(int del_id) { //--- Close current handle before rewriting if(g_ai_logFileHandle != INVALID_HANDLE) FileClose(g_ai_logFileHandle); //--- Open temp output file string tempFile = "Temp_Log.txt"; int readHandle = FileOpen(LogFileName, FILE_READ | FILE_TXT); if(readHandle == INVALID_HANDLE) { Print("Failed to open log for reading: ", GetLastError()); AiReopenLogHandle(); return; } int writeHandle = FileOpen(tempFile, FILE_WRITE | FILE_TXT); if(writeHandle == INVALID_HANDLE) { Print("Failed to open temp log: ", GetLastError()); FileClose(readHandle); AiReopenLogHandle(); return; } //--- Stream lines, skipping the deleted chat's block bool skipBlock = false; while(!FileIsEnding(readHandle)) { string line = FileReadString(readHandle); if(StringFind(line, "Chat ID: ") == 0) { skipBlock = false; int commaPos = StringFind(line, ", Title: "); if(commaPos > 0) { string idStr = StringSubstr(line, 9, commaPos - 9); int chatId = (int)StringToInteger(idStr); if(chatId == del_id) skipBlock = true; } } if(!skipBlock) FileWrite(writeHandle, line); } //--- Close, swap files, reopen FileClose(readHandle); FileClose(writeHandle); FileDelete(LogFileName); FileMove(tempFile, 0, LogFileName, 0); AiReopenLogHandle(); } //+------------------------------------------------------------------+ //| Delete a chat by id | //+------------------------------------------------------------------+ bool AiDeleteChat(int id) { //--- Find chat index int idx = AiGetChatIndex(id); if(idx < 0) return false; //--- If deleting active chat, switch to neighbor or fresh if(g_ai_currentChatId == id) { if(ArraySize(g_ai_chats) > 1) { int new_idx = (idx == ArraySize(g_ai_chats) - 1) ? idx - 1 : ArraySize(g_ai_chats) - 1; g_ai_currentChatId = g_ai_chats[new_idx].id; g_ai_currentTitle = g_ai_chats[new_idx].title; g_ai_conversationHistory = g_ai_chats[new_idx].history; } else { //--- Last chat - replace with a fresh empty one AiCreateNewChat(); return true; } } //--- Remove record ArrayRemove(g_ai_chats, idx, 1); //--- Optionally clear log entries if(DeleteLogsOnChatDelete) AiClearLogsForChat(id); //--- Persist and re-render const bool savedOk = AiSaveChats(); Ai_RenderAll(); ChartRedraw(); return savedOk; } //+------------------------------------------------------------------+ //| Strip trailing whitespace-only lines from a string | //+------------------------------------------------------------------+ string AiStripTrailingBlankLines(string s) { //--- Walk back from end while whitespace int len = StringLen(s); while(len > 0) { ushort ch = StringGetCharacter(s, len - 1); if(ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') len--; else break; } if(len <= 0) return ""; return StringSubstr(s, 0, len); } //+------------------------------------------------------------------+ //| Strip leading whitespace-only lines from a string | //+------------------------------------------------------------------+ string AiStripLeadingBlankLines(string s) { //--- Walk forward from start while whitespace const int len = StringLen(s); int start = 0; while(start < len) { const ushort ch = StringGetCharacter(s, start); if(ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') start++; else break; } if(start >= len) return ""; return StringSubstr(s, start); } //+------------------------------------------------------------------+ //| Get the most recent user prompt from conversation history | //+------------------------------------------------------------------+ string AiGetLastUserPrompt() { //--- Split history into lines string lines[]; const int num_lines = StringSplit(g_ai_conversationHistory, '\n', lines); if(num_lines == 0) return ""; //--- Find the most recent "You: " line int userStart = -1; for(int i = num_lines - 1; i >= 0; i--) { string t = lines[i]; StringTrimLeft(t); StringTrimRight(t); if(StringFind(t, "You: ") == 0) { userStart = i; break; } } if(userStart < 0) return ""; //--- Extract first content line string firstLine = lines[userStart]; const int prefixPos = StringFind(firstLine, "You: "); string prompt = (prefixPos >= 0) ? StringSubstr(firstLine, prefixPos + 5) : firstLine; //--- Append continuation lines until block boundary for(int j = userStart + 1; j < num_lines; j++) { string raw = lines[j]; string t = raw; StringTrimLeft(t); StringTrimRight(t); if(AiIsTimestamp(t)) break; if(StringFind(t, "You: ") == 0) break; if(StringFind(t, "AI: ") == 0) break; prompt += "\n" + raw; } //--- Strip surrounding blank lines prompt = AiStripLeadingBlankLines(prompt); return AiStripTrailingBlankLines(prompt); } //+------------------------------------------------------------------+ //| Get the most recent AI response from conversation history | //+------------------------------------------------------------------+ string AiGetLastAIResponse() { //--- Split history into lines string lines[]; int num_lines = StringSplit(g_ai_conversationHistory, '\n', lines); if(num_lines == 0) return ""; //--- Find the most recent "AI: " line int ai_start = -1; for(int i = num_lines - 1; i >= 0; i--) { string trimmed = lines[i]; StringTrimLeft(trimmed); StringTrimRight(trimmed); if(StringFind(trimmed, "AI: ") == 0) { ai_start = i; break; } } if(ai_start == -1) return ""; //--- Extract first content line string response_build = ""; string first_line = lines[ai_start]; int prefix_pos = StringFind(first_line, "AI: "); if(prefix_pos >= 0) { first_line = StringSubstr(first_line, prefix_pos + 4); StringTrimLeft(first_line); StringTrimRight(first_line); if(StringLen(first_line) > 0 && StringFind(first_line, "(Response in ") != 0 && StringFind(first_line, "(Regenerated in ") != 0 && !AiIsTimestamp(first_line)) { response_build = first_line; } } //--- Append continuation lines until block boundary for(int j = ai_start + 1; j < num_lines; j++) { string orig_line = lines[j]; string trimmed = orig_line; StringTrimLeft(trimmed); StringTrimRight(trimmed); if(StringFind(trimmed, "You: ") == 0 || StringFind(trimmed, "AI: ") == 0) break; if(StringFind(trimmed, "(Response in ") == 0 || StringFind(trimmed, "(Regenerated in ") == 0 || AiIsTimestamp(trimmed)) continue; if(response_build != "") response_build += "\n"; response_build += orig_line; } return response_build; } //+------------------------------------------------------------------+ //| Remove the last AI response block | //+------------------------------------------------------------------+ void AiRemoveLastAIResponse() { //--- Split into double-newline blocks string blocks[]; int num_blocks = AiSplitOnString(g_ai_conversationHistory, "\n\n", blocks); if(num_blocks == 0) return; //--- Drop trailing AI block if present if(StringFind(blocks[num_blocks - 1], "AI: ") == 0) { string new_history = ""; for(int i = 0; i < num_blocks - 1; i++) { if(new_history != "") new_history += "\n\n"; new_history += blocks[i]; } g_ai_conversationHistory = new_history; } //--- Persist updated history AiUpdateCurrentHistory(); } //+------------------------------------------------------------------+ //| Remove the most recent user+AI turn pair | //+------------------------------------------------------------------+ void AiRemoveLastConversationTurn() { //--- Split history into lines string lines[]; const int num_lines = StringSplit(g_ai_conversationHistory, '\n', lines); if(num_lines == 0) return; //--- Find last "AI: " line int aiStart = -1; for(int i = num_lines - 1; i >= 0; i--) { string t = lines[i]; StringTrimLeft(t); StringTrimRight(t); if(StringFind(t, "AI: ") == 0) { aiStart = i; break; } } //--- Find matching "You: " line before it int youStart = -1; const int searchUpTo = (aiStart < 0) ? num_lines - 1 : aiStart - 1; for(int j = searchUpTo; j >= 0; j--) { string t = lines[j]; StringTrimLeft(t); StringTrimRight(t); if(StringFind(t, "You: ") == 0) { youStart = j; break; } } if(youStart < 0) return; //--- Strip trailing blank lines from kept range int keepEnd = youStart; while(keepEnd > 0) { string t = lines[keepEnd - 1]; StringTrimLeft(t); StringTrimRight(t); if(StringLen(t) == 0) keepEnd--; else break; } //--- Rebuild history from kept lines string new_history = ""; for(int k = 0; k < keepEnd; k++) { if(k > 0) new_history += "\n"; new_history += lines[k]; } //--- Append separator for next message if(StringLen(new_history) > 0) new_history += "\n\n"; g_ai_conversationHistory = new_history; AiUpdateCurrentHistory(); } //--- Object name prefix for all AI signal drawings #define AI_SIGNAL_OBJ_PREFIX "AI_Signal_" //+------------------------------------------------------------------+ //| Create horizontal line on chart | //+------------------------------------------------------------------+ void AiCreateHLine(string name, double price, color clr, ENUM_LINE_STYLE style, string tooltip) { //--- Create and configure horizontal line ObjectCreate(0, name, OBJ_HLINE, 0, 0, price); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_STYLE, style); ObjectSetInteger(0, name, OBJPROP_WIDTH, 1); ObjectSetString (0, name, OBJPROP_TEXT, tooltip); } //+------------------------------------------------------------------+ //| Create text label on chart | //+------------------------------------------------------------------+ void AiCreateTextLabel(string name, datetime time, double price, string text, color clr, ENUM_ANCHOR_POINT anchor, double angle = 0) { //--- Create and configure text object ObjectCreate(0, name, OBJ_TEXT, 0, time, price); ObjectSetString (0, name, OBJPROP_TEXT, text); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 10); ObjectSetString (0, name, OBJPROP_FONT, "Arial Bold"); ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetDouble (0, name, OBJPROP_ANGLE, angle); } //+------------------------------------------------------------------+ //| Compute volatility-based price buffer | //+------------------------------------------------------------------+ double Ai_DynamicBuffer(int bars = 14, double fraction = 0.2) { //--- Guard against invalid input if(bars <= 0) return 10.0 * _Point; //--- Pull recent bars MqlRates r[]; int copied = CopyRates(Symbol(), Period(), 1, bars, r); if(copied <= 0) return 10.0 * _Point; //--- Compute average bar range double sum = 0.0; for(int i = 0; i < copied; i++) sum += (r[i].high - r[i].low); double avgRange = sum / (double)copied; double buf = avgRange * fraction; //--- Floor at one tick const double minBuf = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE); if(buf < minBuf) buf = minBuf; return buf; } //+------------------------------------------------------------------+ //| Get short label for an order type | //+------------------------------------------------------------------+ string Ai_OrderTypeLabel(ENUM_ORDER_TYPE t) { //--- Map order type to readable label if(t == ORDER_TYPE_BUY) return "BUY (market)"; if(t == ORDER_TYPE_SELL) return "SELL (market)"; if(t == ORDER_TYPE_BUY_LIMIT) return "BUY LIMIT"; if(t == ORDER_TYPE_SELL_LIMIT) return "SELL LIMIT"; if(t == ORDER_TYPE_BUY_STOP) return "BUY STOP"; if(t == ORDER_TYPE_SELL_STOP) return "SELL STOP"; return "UNKNOWN"; } //+------------------------------------------------------------------+ //| Place market or pending order with broker validation | //+------------------------------------------------------------------+ void Ai_PlaceOrder(ENUM_ORDER_TYPE orderType, double triggerPrice, double sl, double tp, string comment, bool &success, ulong &ticket, string &errorMsg) { //--- Initialize outputs success = false; ticket = 0; errorMsg = ""; //--- Read symbol info const string sym = Symbol(); const double bid = SymbolInfoDouble(sym, SYMBOL_BID); const double ask = SymbolInfoDouble(sym, SYMBOL_ASK); const double pt = SymbolInfoDouble(sym, SYMBOL_POINT); const long stopsLvl = SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL); const double minDist = (double)stopsLvl * pt; //--- Validate pending order trigger direction if(orderType == ORDER_TYPE_BUY_LIMIT) { if(triggerPrice >= ask - minDist) { errorMsg = "BUY LIMIT must be below current ask by at least stops level"; return; } } else if(orderType == ORDER_TYPE_SELL_LIMIT) { if(triggerPrice <= bid + minDist) { errorMsg = "SELL LIMIT must be above current bid by at least stops level"; return; } } else if(orderType == ORDER_TYPE_BUY_STOP) { if(triggerPrice <= ask + minDist) { errorMsg = "BUY STOP must be above current ask by at least stops level"; return; } } else if(orderType == ORDER_TYPE_SELL_STOP) { if(triggerPrice >= bid - minDist) { errorMsg = "SELL STOP must be below current bid by at least stops level"; return; } } //--- Determine direction and reference price const bool isLong = (orderType == ORDER_TYPE_BUY || orderType == ORDER_TYPE_BUY_LIMIT || orderType == ORDER_TYPE_BUY_STOP); const double refPx = (orderType == ORDER_TYPE_BUY) ? ask : (orderType == ORDER_TYPE_SELL) ? bid : triggerPrice; //--- Clamp SL/TP against minimum stops distance if(isLong) { if(sl > 0.0 && (refPx - sl) < minDist) sl = refPx - minDist; if(tp > 0.0 && (tp - refPx) < minDist) tp = refPx + minDist; } else { if(sl > 0.0 && (sl - refPx) < minDist) sl = refPx + minDist; if(tp > 0.0 && (refPx - tp) < minDist) tp = refPx - minDist; } //--- Normalize prices to symbol digits const int digits = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); sl = NormalizeDouble(sl, digits); tp = NormalizeDouble(tp, digits); triggerPrice = NormalizeDouble(triggerPrice, digits); //--- Dispatch to market or pending open bool ok = false; if(orderType == ORDER_TYPE_BUY || orderType == ORDER_TYPE_SELL) { ok = g_ai_trade.PositionOpen(sym, orderType, LotSize, 0.0, sl, tp, comment); } else { ok = g_ai_trade.OrderOpen(sym, orderType, LotSize, 0.0, triggerPrice, sl, tp, ORDER_TIME_GTC, 0, comment); } //--- Capture result if(!ok) { errorMsg = g_ai_trade.ResultRetcodeDescription(); return; } success = true; ticket = g_ai_trade.ResultOrder(); } //+------------------------------------------------------------------+ //| Open trade based on signal text | //+------------------------------------------------------------------+ void AiOpenTrade(string signal, double entry, double sl, double tp) { //--- Map signal to order type ENUM_ORDER_TYPE type = (StringFind(signal, "BUY") >= 0) ? ORDER_TYPE_BUY : (StringFind(signal, "SELL") >= 0) ? ORDER_TYPE_SELL : (ENUM_ORDER_TYPE)-1; if(type == (ENUM_ORDER_TYPE)-1) return; //--- Submit market order double price = 0; string comment = "AI Signal: " + signal; int result = g_ai_trade.PositionOpen(Symbol(), type, LotSize, price, sl, tp, comment); //--- Report outcome if(result == -1) { Print("Trade open failed: ", g_ai_trade.ResultRetcodeDescription()); Alert("Failed to open trade."); } else { Print("Trade opened: Ticket #", g_ai_trade.ResultOrder()); } } //+------------------------------------------------------------------+ //| Visualize trading signal with pattern-specific drawings | //+------------------------------------------------------------------+ void AiVisualizeSignal(string signal, string reason, int triggerBar, double entry, double sl, double tp) { //--- Increment unique signal id static int sigId = 0; sigId++; string prefix = AI_SIGNAL_OBJ_PREFIX + IntegerToString(sigId) + "_"; //--- Pull bars around trigger MqlRates rates[]; int barsToCopy = 5; if(CopyRates(Symbol(), Period(), triggerBar, barsToCopy, rates) < barsToCopy) { Print("Failed to get rates for complete bar ", triggerBar); return; } ArraySetAsSeries(rates, true); //--- Cache bar data datetime triggerTime = rates[0].time; datetime prevTime = rates[1].time; double triggerHigh = rates[0].high, triggerLow = rates[0].low; double prevHigh = rates[1].high, prevLow = rates[1].low; double triggerOpen = rates[0].open, triggerClose = rates[0].close; //--- Determine arrow style bool isBuy = (StringFind(signal, "BUY") >= 0); int arrowCode = isBuy ? 233 : 234; color arrowColor = isBuy ? clrGreen : clrRed; double arrowPrice = isBuy ? triggerLow : triggerHigh; int arrowAnchor = isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM; //--- Draw signal arrow string arrowName = prefix + "Arrow_" + TimeToString(triggerTime); ObjectCreate(0, arrowName, OBJ_ARROW, 0, triggerTime, arrowPrice); ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, arrowCode); ObjectSetInteger(0, arrowName, OBJPROP_COLOR, arrowColor); ObjectSetInteger(0, arrowName, OBJPROP_ANCHOR, arrowAnchor); ObjectSetInteger(0, arrowName, OBJPROP_BACK, false); //--- Lower-case reason for pattern matching string lowerReason = reason; StringToLower(lowerReason); string patternLabel = reason + " (Bar " + IntegerToString(triggerBar) + ")"; color labelColor = clrBlack; //--- Engulfing pattern visualization if(StringFind(lowerReason, "engulfing") >= 0) { bool isBullish = (StringFind(lowerReason, "bullish") >= 0) || (StringFind(signal, "BUY") >= 0); color engulfColor = isBullish ? clrDarkBlue : clrDarkRed; ObjectCreate(0, prefix + "EngulfRect", OBJ_RECTANGLE, 0, prevTime, prevLow, triggerTime + (PeriodSeconds() / 2), triggerHigh); ObjectSetInteger(0, prefix + "EngulfRect", OBJPROP_COLOR, engulfColor); ObjectSetInteger(0, prefix + "EngulfRect", OBJPROP_FILL, true); ObjectSetInteger(0, prefix + "EngulfRect", OBJPROP_BACK, true); ObjectSetInteger(0, prefix + "EngulfRect", OBJPROP_WIDTH, 1); patternLabel = (isBullish ? "Bullish " : "Bearish ") + "Engulfing (Bar " + IntegerToString(triggerBar) + ")"; labelColor = clrBlue; } //--- Pin bar / hammer / shooting star visualization else if(StringFind(lowerReason, "pin bar") >= 0 || StringFind(lowerReason, "hammer") >= 0 || StringFind(lowerReason, "shooting star") >= 0) { color pinColor = isBuy ? clrGreen : clrRed; ObjectCreate(0, prefix + "PinLine", OBJ_TREND, 0, triggerTime, triggerOpen, triggerTime + PeriodSeconds(), triggerClose); ObjectSetInteger(0, prefix + "PinLine", OBJPROP_COLOR, pinColor); ObjectSetInteger(0, prefix + "PinLine", OBJPROP_WIDTH, 2); ObjectSetInteger(0, prefix + "PinLine", OBJPROP_RAY_RIGHT, false); string patName = StringFind(lowerReason, "hammer") >= 0 ? "Hammer" : StringFind(lowerReason, "shooting star") >= 0 ? "Shooting Star" : "Pin Bar"; patternLabel = patName + " (Bar " + IntegerToString(triggerBar) + ")"; labelColor = pinColor; } //--- Inside bar visualization else if(StringFind(lowerReason, "inside bar") >= 0) { color insideColor = clrYellow; ObjectCreate(0, prefix + "InsideRect", OBJ_RECTANGLE, 0, triggerTime, triggerLow, triggerTime + PeriodSeconds(), triggerHigh); ObjectSetInteger(0, prefix + "InsideRect", OBJPROP_COLOR, insideColor); ObjectSetInteger(0, prefix + "InsideRect", OBJPROP_FILL, false); ObjectSetInteger(0, prefix + "InsideRect", OBJPROP_BACK, true); ObjectSetInteger(0, prefix + "InsideRect", OBJPROP_STYLE, STYLE_DASH); patternLabel = "Inside Bar (Bar " + IntegerToString(triggerBar) + ")"; labelColor = clrOrange; } //--- Doji visualization else if(StringFind(lowerReason, "doji") >= 0) { color dojiColor = clrMagenta; ObjectCreate(0, prefix + "DojiMark", OBJ_TEXT, 0, triggerTime, (triggerHigh + triggerLow) / 2); ObjectSetString (0, prefix + "DojiMark", OBJPROP_FONT, "Wingdings"); ObjectSetInteger(0, prefix + "DojiMark", OBJPROP_FONTSIZE, 12); ObjectSetString (0, prefix + "DojiMark", OBJPROP_TEXT, CharToString((uchar)108)); ObjectSetInteger(0, prefix + "DojiMark", OBJPROP_COLOR, dojiColor); patternLabel = "Doji (Bar " + IntegerToString(triggerBar) + ")"; labelColor = dojiColor; } //--- Three soldiers / three crows visualization else if(StringFind(lowerReason, "three white soldiers") >= 0 || StringFind(lowerReason, "three black crows") >= 0) { bool isBullish = StringFind(lowerReason, "white soldiers") >= 0; color soldiersColor = isBullish ? clrLimeGreen : clrCrimson; datetime startTime = rates[2].time; ObjectCreate(0, prefix + "SoldiersRect", OBJ_RECTANGLE, 0, startTime, rates[2].low, triggerTime + PeriodSeconds(), rates[0].high); ObjectSetInteger(0, prefix + "SoldiersRect", OBJPROP_COLOR, soldiersColor); ObjectSetInteger(0, prefix + "SoldiersRect", OBJPROP_FILL, true); ObjectSetInteger(0, prefix + "SoldiersRect", OBJPROP_BACK, true); ObjectSetInteger(0, prefix + "SoldiersRect", OBJPROP_WIDTH, 1); patternLabel = (isBullish ? "Three White Soldiers" : "Three Black Crows") + " (Bars " + IntegerToString(triggerBar - 2) + "-" + IntegerToString(triggerBar) + ")"; labelColor = soldiersColor; } //--- Double top / double bottom visualization else if(StringFind(lowerReason, "double bottom") >= 0 || StringFind(lowerReason, "double top") >= 0) { bool isBottom = StringFind(lowerReason, "bottom") >= 0; double level = isBottom ? MathMin(rates[1].low, triggerLow) : MathMax(rates[1].high, triggerHigh); color doubleColor = isBottom ? clrAqua : clrFuchsia; ObjectCreate(0, prefix + "DoubleLine", OBJ_TREND, 0, rates[1].time, level, triggerTime + PeriodSeconds() * 3, level); ObjectSetInteger(0, prefix + "DoubleLine", OBJPROP_COLOR, doubleColor); ObjectSetInteger(0, prefix + "DoubleLine", OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, prefix + "DoubleLine", OBJPROP_WIDTH, 2); ObjectSetInteger(0, prefix + "DoubleLine", OBJPROP_RAY_RIGHT, true); patternLabel = (isBottom ? "Double Bottom" : "Double Top") + " (Bar " + IntegerToString(triggerBar) + ") at " + DoubleToString(level, _Digits); labelColor = doubleColor; } //--- Breakout visualization else if(StringFind(lowerReason, "breakout") >= 0) { double breakLevel = isBuy ? MathMax(prevHigh, triggerHigh) : MathMin(prevLow, triggerLow); ObjectCreate(0, prefix + "BreakLine", OBJ_TREND, 0, prevTime, breakLevel, triggerTime + PeriodSeconds() * 5, breakLevel); ObjectSetInteger(0, prefix + "BreakLine", OBJPROP_COLOR, clrGray); ObjectSetInteger(0, prefix + "BreakLine", OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, prefix + "BreakLine", OBJPROP_WIDTH, 2); ObjectSetInteger(0, prefix + "BreakLine", OBJPROP_RAY_RIGHT, true); patternLabel = "Breakout (Bar " + IntegerToString(triggerBar) + ") at " + DoubleToString(breakLevel, _Digits); labelColor = clrBlue; } //--- RSI / divergence visualization else if(StringFind(lowerReason, "divergence") >= 0 || StringFind(lowerReason, "rsi") >= 0) { int rsiHandle = iRSI(Symbol(), Period(), 14, PRICE_CLOSE); double rsiVal[1]; if(CopyBuffer(rsiHandle, 0, triggerBar, 1, rsiVal) == 1) { patternLabel = "RSI Divergence (Bar " + IntegerToString(triggerBar) + ", RSI=" + IntegerToString((int)rsiVal[0]) + ")"; } labelColor = clrDarkBlue; IndicatorRelease(rsiHandle); } //--- Generic highlight fallback else { ObjectCreate(0, prefix + "Highlight", OBJ_RECTANGLE, 0, triggerTime, triggerLow, triggerTime + PeriodSeconds(), triggerHigh); ObjectSetInteger(0, prefix + "Highlight", OBJPROP_COLOR, clrLightGray); ObjectSetInteger(0, prefix + "Highlight", OBJPROP_FILL, true); ObjectSetInteger(0, prefix + "Highlight", OBJPROP_BACK, true); patternLabel = reason + " (Bar " + IntegerToString(triggerBar) + ")"; labelColor = clrBlack; } //--- Draw rotated pattern label datetime labelTime = triggerTime; double labelPrice = arrowPrice; int direction = isBuy ? 1 : -1; ENUM_ANCHOR_POINT labelAnchor = ANCHOR_LEFT; double labelAngle = (direction > 0) ? -90 : 90; string labelText = " " + patternLabel; AiCreateTextLabel(prefix + "PatternLabel", labelTime, labelPrice, labelText, labelColor, labelAnchor, labelAngle); Print("Anchored viz drawn: Arrow at ", (isBuy ? "low" : "high"), ", Label: ", patternLabel); ChartRedraw(); } //--- Forward declaration for regenerate path void AiSubmitMessage(string prompt); //+------------------------------------------------------------------+ //| Push signal action turn into chat history | //+------------------------------------------------------------------+ void Ai_PushSignalActionToChat(string userLabel, string aiBubbleText, ulong elapsedMs) { //--- Build timestamp and elapsed note string ts = TimeToString(TimeCurrent(), TIME_MINUTES); int elapsedSec = (int)(elapsedMs / 1000); string timeNote = "\n(Response in " + IntegerToString(elapsedSec) + "s)"; //--- Append user and AI turn to history g_ai_conversationHistory += "You: " + userLabel + "\n" + ts + "\n\n" + "AI: " + aiBubbleText + timeNote + "\n" + ts + "\n\n"; g_ai_chatScrollPin = true; //--- Auto-rename placeholder title to action label if(StringFind(g_ai_currentTitle, "Chat ") == 0) { g_ai_currentTitle = userLabel; } //--- Persist update AiUpdateCurrentHistory(); } //+------------------------------------------------------------------+ //| Auto-generate a concise chat title via secondary AI call | //+------------------------------------------------------------------+ void Ai_AutoTitleChat(string userPrompt, string aiResponse) { //--- Build title-generation prompt string prompt = "Generate a concise title for this conversation. Rules:\n" " - 3 to 5 words ONLY.\n" " - MAXIMUM 100 characters total.\n" " - Plain text - no quotes, no markdown, no trailing punctuation.\n" " - Reflect the topic / intent, not just echo the prompt verbatim.\n" " - Output ONLY the title itself. No prefix like 'Title:' and no commentary.\n" "\n" "USER PROMPT: " + userPrompt + "\n" "AI RESPONSE: " + aiResponse + "\n" "\n" "Title:"; //--- Send request with low temperature string raw = AiGetChatGPTResponse(prompt, 0.3); if(StringLen(raw) == 0) return; //--- Cleanup whitespace and newlines string title = raw; StringReplace(title, "\r", " "); StringReplace(title, "\n", " "); StringReplace(title, " ", " "); StringTrimLeft(title); StringTrimRight(title); //--- Strip wrapping quotes while(StringLen(title) >= 2) { ushort first = StringGetCharacter(title, 0); ushort last = StringGetCharacter(title, StringLen(title) - 1); if((first == '"' && last == '"') || (first == '\'' && last == '\'')) { title = StringSubstr(title, 1, StringLen(title) - 2); StringTrimLeft(title); StringTrimRight(title); } else break; } //--- Strip leading "Title:" prefix if(StringFind(title, "Title:") == 0) { title = StringSubstr(title, 6); StringTrimLeft(title); } else if(StringFind(title, "title:") == 0) { title = StringSubstr(title, 6); StringTrimLeft(title); } //--- Hard cap at 100 characters if(StringLen(title) > 100) { title = StringSubstr(title, 0, 97) + "..."; } //--- Reject empty result StringTrimLeft(title); StringTrimRight(title); if(StringLen(title) == 0) return; //--- Apply and persist title g_ai_currentTitle = title; AiUpdateCurrentHistory(); Ai_RenderAll(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Parse KEY: VALUE line from AI response | //+------------------------------------------------------------------+ string Ai_ParseKVResponse(string raw, string key) { //--- Split into lines and uppercase target key string lines[]; int n = StringSplit(raw, '\n', lines); string keyUpper = key; StringToUpper(keyUpper); //--- Search for matching key for(int i = 0; i < n; i++) { string line = lines[i]; StringTrimLeft(line); StringTrimRight(line); int colonPos = StringFind(line, ":"); if(colonPos <= 0) continue; string lineKey = StringSubstr(line, 0, colonPos); StringTrimLeft(lineKey); StringTrimRight(lineKey); StringToUpper(lineKey); if(lineKey != keyUpper) continue; string val = StringSubstr(line, colonPos + 1); StringTrimLeft(val); StringTrimRight(val); return val; } return ""; } //+------------------------------------------------------------------+ //| Build standard bar-definition preamble for AI prompts | //+------------------------------------------------------------------+ string Ai_BuildBarPreamble() { //--- Return shared bar rules text return "BAR DEFINITIONS (read carefully before analyzing):\n" " - A bar is BULLISH if its close > open.\n" " - A bar is BEARISH if its close < open.\n" " - A bar is DOJI if close equals open.\n" "\n" "BAR ORDERING IN THE DATA BELOW (CRITICAL):\n" " - Bars are listed in DESCENDING TIME ORDER.\n" " - Bar 1 = MOST RECENT closed bar (highest timestamp, just finished).\n" " - Bar 2 = the bar that closed BEFORE Bar 1.\n" " - Bar 3 closed before Bar 2, and so on backwards in time.\n" " - Bar 1 is the RIGHTMOST candle on the chart.\n" " - The data block includes each bar's timestamp so you can verify ordering.\n" " - DO NOT reverse this. Bar 1 is NEWEST, Bar N is OLDEST.\n" "\n"; } //+------------------------------------------------------------------+ //| Format N bars in descending time order for AI prompt | //+------------------------------------------------------------------+ string Ai_FormatBarsDesc(ENUM_TIMEFRAMES tf, int count) { //--- Pull rates and order newest first MqlRates r[]; if(CopyRates(Symbol(), tf, 1, count, r) < count) return ""; ArraySetAsSeries(r, true); //--- Build header string out = "BAR DATA (Bar 1 = newest, Bar " + IntegerToString(count) + " = oldest):\n"; //--- Append each bar with direction tag for(int i = 0; i < count; i++) { string dir = (r[i].close > r[i].open) ? "BULLISH" : (r[i].close < r[i].open) ? "BEARISH" : "DOJI"; out += "Bar " + IntegerToString(i + 1) + " | Time=" + TimeToString(r[i].time, TIME_DATE | TIME_MINUTES) + " | O=" + DoubleToString(r[i].open, _Digits) + " | H=" + DoubleToString(r[i].high, _Digits) + " | L=" + DoubleToString(r[i].low, _Digits) + " | C=" + DoubleToString(r[i].close, _Digits) + " | (this bar is " + dir + ")\n"; } return out; } //+------------------------------------------------------------------+ //| Draw signal arrow on current chart with optional label | //+------------------------------------------------------------------+ void Ai_DrawSignalArrow(string actionTag, string signal, double entryPrice, string labelText = "") { //--- Bail on non-directional signal if(signal != "BUY" && signal != "SELL") return; //--- Increment unique sequence id static int s_arrowSeq = 0; s_arrowSeq++; bool isBuy = (signal == "BUY"); //--- Anchor to most recent closed bar on current chart datetime t = iTime(Symbol(), Period(), 1); double bH = iHigh(Symbol(), Period(), 1); double bL = iLow (Symbol(), Period(), 1); double arrowPrice = isBuy ? bL : bH; //--- Create arrow object string name = AI_SIGNAL_OBJ_PREFIX + actionTag + "_Arrow_" + IntegerToString(s_arrowSeq); if(ObjectFind(0, name) >= 0) ObjectDelete(0, name); ObjectCreate(0, name, OBJ_ARROW, 0, t, arrowPrice); ObjectSetInteger(0, name, OBJPROP_ARROWCODE, isBuy ? 233 : 234); ObjectSetInteger(0, name, OBJPROP_COLOR, isBuy ? clrGreen : clrRed); ObjectSetInteger(0, name, OBJPROP_ANCHOR, isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM); ObjectSetInteger(0, name, OBJPROP_BACK, false); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); //--- Draw rotated label if provided if(StringLen(labelText) > 0) { string lblName = AI_SIGNAL_OBJ_PREFIX + actionTag + "_Label_" + IntegerToString(s_arrowSeq); if(ObjectFind(0, lblName) >= 0) ObjectDelete(0, lblName); const color labelColor = isBuy ? clrGreen : clrRed; const double labelAngle = isBuy ? -90.0 : 90.0; AiCreateTextLabel(lblName, t, arrowPrice, " " + labelText, labelColor, ANCHOR_LEFT, labelAngle); } ChartRedraw(); } //+------------------------------------------------------------------+ //| Twin Bars action - signal from last 2 bars on current timeframe | //+------------------------------------------------------------------+ void AiTwinBars() { //--- Throttle concurrent requests if(g_ai_signalRequestInFlight) { Ai_ShowToast("Signal in progress - please wait", true); return; } g_ai_signalRequestInFlight = true; //--- Pull Bar 1 close as fallback entry MqlRates rfb[]; if(CopyRates(Symbol(), Period(), 1, 1, rfb) < 1) { g_ai_signalRequestInFlight = false; Ai_ShowToast("Twin Bars: failed to load Bar 1", true); return; } ArraySetAsSeries(rfb, true); double bar1Close = rfb[0].close; //--- Build bars block string barsBlock = Ai_FormatBarsDesc(Period(), 2); if(StringLen(barsBlock) == 0) { g_ai_signalRequestInFlight = false; Ai_ShowToast("Twin Bars: failed to load bars", true); return; } //--- Build prompt with strict twin-bars rule string prompt = "You are a precise trading-rule executor. Apply the rule below to the 2 bars in the DATA section.\n" "\n" + Ai_BuildBarPreamble() + "STRICT RULE FOR TWIN BARS:\n" " - If BOTH Bar 1 AND Bar 2 are BULLISH (close > open) -> SIGNAL is BUY.\n" " - If BOTH Bar 1 AND Bar 2 are BEARISH (close < open) -> SIGNAL is SELL.\n" " - Otherwise (any mix, any DOJI) -> SIGNAL is NONE.\n" "\n" "OUTPUT FORMAT (LINE-BASED KEY: VALUE - NO JSON, NO MARKDOWN, NO PROSE):\n" "Output EXACTLY 3 lines, in this order:\n" "SIGNAL: \n" "REASON: Bar 1 O= C= -> ; Bar 2 O= C= -> ; Decision: BUY | both BEARISH=>SELL | mixed=>NONE>\n" "ENTRY: \n" "\n" "EXAMPLE (do not copy values, use the actual ones from DATA):\n" "SIGNAL: BUY\n" "REASON: Bar 1 O=1.10100 C=1.10250 -> BULLISH; Bar 2 O=1.10000 C=1.10100 -> BULLISH; Decision: both BULLISH=>BUY\n" "ENTRY: 1.10250\n" "\n" "DATA:\n" + barsBlock; //--- Time the call ulong t0 = GetTickCount(); string resp = AiGetChatGPTResponse(prompt, 0.0); ulong elapsed = GetTickCount() - t0; //--- Parse response string signal = Ai_ParseKVResponse(resp, "SIGNAL"); string reason = Ai_ParseKVResponse(resp, "REASON"); string entryStr = Ai_ParseKVResponse(resp, "ENTRY"); double entry = StringToDouble(entryStr); StringToUpper(signal); //--- Apply local fallback if entry missing if((signal == "BUY" || signal == "SELL") && entry <= 0.0) { entry = bar1Close; } if(signal != "BUY" && signal != "SELL") signal = "NONE"; //--- Build chat bubble string bubble; if(signal == "BUY") bubble = "Twin Bars -> BUY at " + DoubleToString(entry, _Digits) + "\n" + reason; else if(signal == "SELL") bubble = "Twin Bars -> SELL at " + DoubleToString(entry, _Digits) + "\n" + reason; else bubble = "Twin Bars -> NONE\n" + reason; //--- Auto-trade if enabled if(AutoTrade && (signal == "BUY" || signal == "SELL")) { const double bar1High = rfb[0].high; const double bar1Low = rfb[0].low; const double buf = Ai_DynamicBuffer(); double sl = 0.0, tp = 0.0; if(signal == "BUY") { sl = bar1Low - buf; tp = entry + 2.0 * (entry - sl); } else { sl = bar1High + buf; tp = entry - 2.0 * (sl - entry); } const ENUM_ORDER_TYPE ot = (signal == "BUY") ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; bool ok = false; ulong tk = 0; string err = ""; Ai_PlaceOrder(ot, 0.0, sl, tp, "Twin Bars", ok, tk, err); if(ok) { bubble += "\nTrade: " + Ai_OrderTypeLabel(ot) + " @ " + DoubleToString(entry, _Digits) + " | SL=" + DoubleToString(sl, _Digits) + " | TP=" + DoubleToString(tp, _Digits) + " | Ticket #" + IntegerToString((long)tk); } else { bubble += "\nTrade FAILED: " + err; Ai_ShowToast("Twin Bars trade failed: " + err, true); } } //--- Push to chat and draw arrow Ai_PushSignalActionToChat("Twin Bars", bubble, elapsed); Ai_DrawSignalArrow("TwinBars", signal, entry, "Twin Bars " + signal + " @ " + DoubleToString(entry, _Digits)); g_ai_signalRequestInFlight = false; } //+------------------------------------------------------------------+ //| Daily Signal action - signal from today's H1 bars | //+------------------------------------------------------------------+ void AiDailySignal() { //--- Throttle concurrent requests if(g_ai_signalRequestInFlight) { Ai_ShowToast("Signal in progress - please wait", true); return; } g_ai_signalRequestInFlight = true; //--- Pull last closed H1 bar as fallback entry source MqlRates rfb[]; if(CopyRates(Symbol(), PERIOD_H1, 1, 1, rfb) < 1) { g_ai_signalRequestInFlight = false; Ai_ShowToast("Daily Signal: failed to load H1 Bar 1", true); return; } ArraySetAsSeries(rfb, true); double h1Close = rfb[0].close; //--- Compute server midnight timestamp datetime now = TimeCurrent(); MqlDateTime dt; TimeToStruct(now, dt); dt.hour = 0; dt.min = 0; dt.sec = 0; datetime midnight = StructToTime(dt); //--- Compute current H1 bar start MqlDateTime hourCap; TimeToStruct(now, hourCap); hourCap.min = 0; hourCap.sec = 0; datetime currentH1Start = StructToTime(hourCap); //--- Stop one second before in-progress bar datetime stopTime = currentH1Start - 1; //--- Bail if no closed H1 bars yet today if(stopTime < midnight) { g_ai_signalRequestInFlight = false; Ai_ShowToast("Daily Signal: no closed H1 bars for today yet", false); return; } //--- Compute expected closed bar count const int expectedBars = (int)((currentH1Start - midnight) / 3600); //--- Pull H1 bars with retry for missing history MqlRates rd[]; int copied = 0; for(int retry = 0; retry < 5; retry++) { copied = CopyRates(Symbol(), PERIOD_H1, midnight, stopTime, rd); if(copied >= expectedBars) break; Sleep(200); } if(copied <= 0) { g_ai_signalRequestInFlight = false; Ai_ShowToast("Daily Signal: no H1 bars for today yet", true); return; } //--- Warn if incomplete if(copied < expectedBars) { Print("Daily Signal warning: expected ", expectedBars, " H1 bars from midnight, got ", copied, ". H1 history may be incomplete."); } if(copied > 24) copied = 24; //--- Order newest first ArraySetAsSeries(rd, true); //--- Build bars block string barsBlock = "BAR DATA - " + IntegerToString(copied) + " CLOSED H1 bar(s) from midnight today up to the most recently CLOSED H1 bar:\n" + "(Bar 1 = most recent closed H1, Bar " + IntegerToString(copied) + " = the H1 bar that opened at midnight)\n"; for(int i = 0; i < copied; i++) { string dir = (rd[i].close > rd[i].open) ? "BULLISH" : (rd[i].close < rd[i].open) ? "BEARISH" : "DOJI"; barsBlock += "Bar " + IntegerToString(i + 1) + " | Time=" + TimeToString(rd[i].time, TIME_DATE | TIME_MINUTES) + " | O=" + DoubleToString(rd[i].open, _Digits) + " | H=" + DoubleToString(rd[i].high, _Digits) + " | L=" + DoubleToString(rd[i].low, _Digits) + " | C=" + DoubleToString(rd[i].close, _Digits) + " | (" + dir + ")\n"; } //--- Build daily signal prompt string prompt = "You are a precise trading analyst. Below are today's H1 bars from server midnight.\n" "\n" + Ai_BuildBarPreamble() + "DECISION RULES FOR DAILY SIGNAL:\n" " - You MUST analyze ALL of the H1 bars provided in the DATA section, not just a few.\n" " - SIGNAL is BUY only if today's H1 bars show CLEAR upward continuation/momentum (most bars BULLISH, higher closes, no strong rejection).\n" " - SIGNAL is SELL only if today's H1 bars show CLEAR downward continuation/momentum.\n" " - SIGNAL is NONE if mixed, choppy, or unclear.\n" "\n" "REASON FORMATTING - REQUIRED:\n" " Your REASON must include ALL of the following, in this order:\n" " (a) State how many bars you analyzed (e.g. 'Analyzed 14 H1 bars').\n" " (b) State the count of BULLISH vs BEARISH vs DOJI bars (e.g. 'Bullish: 9, Bearish: 4, Doji: 1').\n" " (c) State the open of the OLDEST bar (Bar N) and the close of the NEWEST bar (Bar 1) to show net day movement.\n" " (d) Cite the 2-3 most decisive bars (largest range, key reversals, breakouts) by number with brief reasoning.\n" " (e) State the conclusion connecting the evidence to the SIGNAL.\n" "\n" "OUTPUT FORMAT (LINE-BASED KEY: VALUE - NO JSON, NO MARKDOWN, NO PROSE):\n" "Output EXACTLY 3 lines, in this order:\n" "SIGNAL: \n" "REASON: \n" "ENTRY: \n" "\n" "EXAMPLE (replace values with actual ones from DATA - note how it covers ALL bars, not just 2-3):\n" "SIGNAL: BUY\n" "REASON: Analyzed 14 H1 bars; Bullish: 10, Bearish: 3, Doji: 1; Day opened at 78198.28 (Bar 14), now at 78368.03 (Bar 1) = +169.75 net up; decisive bars: Bar 11 (large bullish breakout, range 95.40), Bar 5 (rejection of low, hammer), Bar 1 (continuation close); conclusion: dominant bullish day with strong momentum and no significant rejection - BUY\n" "ENTRY: 78368.03\n" "\n" "DATA:\n" + barsBlock; //--- Time the call ulong t0 = GetTickCount(); string resp = AiGetChatGPTResponse(prompt, 0.2); ulong elapsed = GetTickCount() - t0; //--- Parse response string signal = Ai_ParseKVResponse(resp, "SIGNAL"); string reason = Ai_ParseKVResponse(resp, "REASON"); string entryStr = Ai_ParseKVResponse(resp, "ENTRY"); double entry = StringToDouble(entryStr); StringToUpper(signal); //--- Apply local fallback if entry missing if((signal == "BUY" || signal == "SELL") && entry <= 0.0) entry = h1Close; if(signal != "BUY" && signal != "SELL") signal = "NONE"; //--- Build chat bubble string bubble; if(signal == "BUY") bubble = "Daily Signal -> BUY at " + DoubleToString(entry, _Digits) + "\n" + reason; else if(signal == "SELL") bubble = "Daily Signal -> SELL at " + DoubleToString(entry, _Digits) + "\n" + reason; else bubble = "Daily Signal -> NONE\n" + reason; //--- Auto-trade if enabled if(AutoTrade && (signal == "BUY" || signal == "SELL")) { const double h1High = rfb[0].high; const double h1Low = rfb[0].low; const double buf = Ai_DynamicBuffer(); double sl = 0.0, tp = 0.0; if(signal == "BUY") { sl = h1Low - buf; tp = entry + 2.0 * (entry - sl); } else { sl = h1High + buf; tp = entry - 2.0 * (sl - entry); } const ENUM_ORDER_TYPE ot = (signal == "BUY") ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; bool ok = false; ulong tk = 0; string err = ""; Ai_PlaceOrder(ot, 0.0, sl, tp, "Daily Signal", ok, tk, err); if(ok) { bubble += "\nTrade: " + Ai_OrderTypeLabel(ot) + " @ " + DoubleToString(entry, _Digits) + " | SL=" + DoubleToString(sl, _Digits) + " | TP=" + DoubleToString(tp, _Digits) + " | Ticket #" + IntegerToString((long)tk); } else { bubble += "\nTrade FAILED: " + err; Ai_ShowToast("Daily Signal trade failed: " + err, true); } } //--- Push to chat and draw arrow Ai_PushSignalActionToChat("Daily Signal", bubble, elapsed); Ai_DrawSignalArrow("DailySignal", signal, entry, "Daily Signal " + signal + " @ " + DoubleToString(entry, _Digits)); g_ai_signalRequestInFlight = false; } //+------------------------------------------------------------------+ //| Trend Read action - trend direction with anchor points | //+------------------------------------------------------------------+ void AiTrendRead() { //--- Throttle concurrent requests if(g_ai_signalRequestInFlight) { Ai_ShowToast("Signal in progress - please wait", true); return; } g_ai_signalRequestInFlight = true; //--- Configure bar count and pull bars const int N = 30; MqlRates ra[]; if(CopyRates(Symbol(), Period(), 1, N, ra) < N) { g_ai_signalRequestInFlight = false; Ai_ShowToast("Trend Read: failed to load bars", true); return; } ArraySetAsSeries(ra, true); //--- Build bars block string barsBlock = Ai_FormatBarsDesc(Period(), N); if(StringLen(barsBlock) == 0) { g_ai_signalRequestInFlight = false; Ai_ShowToast("Trend Read: failed to format bars", true); return; } //--- Build trend read prompt string prompt = "You are a precise trend analyst. Analyze the last " + IntegerToString(N) + " bars in the DATA section.\n" "\n" + Ai_BuildBarPreamble() + "DECISION RULES FOR TREND READ:\n" " - TREND is UP if you see HIGHER HIGHS and HIGHER LOWS in the recent bars (compared to older bars).\n" " - TREND is DOWN if you see LOWER HIGHS and LOWER LOWS in the recent bars.\n" " - TREND is RANGE if highs and lows are roughly horizontal / no clear direction.\n" " - You MUST scan ALL 30 bars to determine the trend, not just a few.\n" "\n" "ANCHOR POINTS FOR THE TRENDLINE:\n" " - ANCHOR1 is the OLDER end (a bar with a higher number, e.g. 18, 22, 30).\n" " - ANCHOR2 is the NEWER end (usually Bar 1).\n" " - For UP trend: anchor through recent HIGHER LOWS (use those bars' LOW prices).\n" " - For DOWN trend: anchor through recent LOWER HIGHS (use those bars' HIGH prices).\n" " - For RANGE: anchor at the range MIDPOINT - use the average of (recent high + recent low).\n" " - Anchor prices MUST come from the actual OHLC values in DATA. Do not invent prices.\n" "\n" "REASON FORMATTING - REQUIRED:\n" " Your REASON must include ALL of the following, in this order:\n" " (a) State how many bars you analyzed (always 30 for this action).\n" " (b) State the highest HIGH and lowest LOW across the set, with their bar numbers.\n" " (c) State net move from oldest bar's open (Bar 30) to newest bar's close (Bar 1).\n" " (d) Describe the swing structure briefly (e.g. 'higher lows and higher highs since Bar 22', or 'choppy oscillation between 1.0980 and 1.1050').\n" " (e) State why you chose those anchor bars and the conclusion.\n" "\n" "OUTPUT FORMAT (LINE-BASED KEY: VALUE - NO JSON, NO MARKDOWN, NO PROSE):\n" "Output EXACTLY 6 lines, in this order:\n" "TREND: \n" "ANCHOR1_BAR: \n" "ANCHOR1_PRICE: \n" "ANCHOR2_BAR: \n" "ANCHOR2_PRICE: \n" "REASON: \n" "\n" "EXAMPLE (replace values with actual ones from DATA - note how it covers ALL 30 bars, not just the anchors):\n" "TREND: UP\n" "ANCHOR1_BAR: 22\n" "ANCHOR1_PRICE: 1.09800\n" "ANCHOR2_BAR: 1\n" "ANCHOR2_PRICE: 1.10250\n" "REASON: Analyzed 30 bars; range was 1.09650 (low at Bar 28) to 1.10310 (high at Bar 4); net move from Bar 30 open 1.09720 to Bar 1 close 1.10250 = +53 pips up; structure shows progressively higher lows from Bar 22 onward (Bar 22 L=1.09800, Bar 15 L=1.09950, Bar 8 L=1.10100, Bar 1 L=1.10220) with no break of prior swing low; anchored Bar 22 as the swing-low origin and Bar 1 as the most recent higher-low confirmation - clear uptrend\n" "\n" "DATA:\n" + barsBlock; //--- Time the call ulong t0 = GetTickCount(); string resp = AiGetChatGPTResponse(prompt, 0.2); ulong elapsed = GetTickCount() - t0; //--- Parse response string trend = Ai_ParseKVResponse(resp, "TREND"); string a1BarStr = Ai_ParseKVResponse(resp, "ANCHOR1_BAR"); string a1PriceStr = Ai_ParseKVResponse(resp, "ANCHOR1_PRICE"); string a2BarStr = Ai_ParseKVResponse(resp, "ANCHOR2_BAR"); string a2PriceStr = Ai_ParseKVResponse(resp, "ANCHOR2_PRICE"); string reason = Ai_ParseKVResponse(resp, "REASON"); //--- Validate trend StringToUpper(trend); if(trend != "UP" && trend != "DOWN" && trend != "RANGE") trend = "RANGE"; //--- Convert anchor inputs int a1Bar = (int)StringToInteger(a1BarStr); int a2Bar = (int)StringToInteger(a2BarStr); double a1Price = StringToDouble(a1PriceStr); double a2Price = StringToDouble(a2PriceStr); //--- Clamp anchor bar indices if(a1Bar < 1) a1Bar = N; if(a1Bar > N) a1Bar = N; if(a2Bar < 1) a2Bar = 1; if(a2Bar > N) a2Bar = N; //--- Apply price fallbacks based on trend if(a1Price <= 0.0) { if(trend == "UP") a1Price = ra[a1Bar - 1].low; else if(trend == "DOWN") a1Price = ra[a1Bar - 1].high; else a1Price = (ra[a1Bar - 1].high + ra[a1Bar - 1].low) / 2.0; } if(a2Price <= 0.0) { if(trend == "UP") a2Price = ra[a2Bar - 1].low; else if(trend == "DOWN") a2Price = ra[a2Bar - 1].high; else a2Price = (ra[a2Bar - 1].high + ra[a2Bar - 1].low) / 2.0; } //--- Build and push chat bubble string bubble = "Trend Read -> " + trend + "\n" + reason; Ai_PushSignalActionToChat("Trend Read", bubble, elapsed); //--- Draw trendline and label if(a1Price > 0 && a2Price > 0) { static int s_trendSeq = 0; s_trendSeq++; datetime t1 = iTime(Symbol(), Period(), a1Bar); datetime t2 = iTime(Symbol(), Period(), a2Bar); color trendColor = (trend == "UP") ? clrLimeGreen : (trend == "DOWN") ? clrCrimson : clrGray; //--- Create trendline object string lineName = AI_SIGNAL_OBJ_PREFIX + "TrendRead_Line_" + IntegerToString(s_trendSeq); if(ObjectFind(0, lineName) >= 0) ObjectDelete(0, lineName); ObjectCreate(0, lineName, OBJ_TREND, 0, t1, a1Price, t2, a2Price); ObjectSetInteger(0, lineName, OBJPROP_COLOR, trendColor); ObjectSetInteger(0, lineName, OBJPROP_WIDTH, 2); ObjectSetInteger(0, lineName, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, lineName, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, lineName, OBJPROP_HIDDEN, true); //--- Create label at newer end string lblName = AI_SIGNAL_OBJ_PREFIX + "TrendRead_Label_" + IntegerToString(s_trendSeq); if(ObjectFind(0, lblName) >= 0) ObjectDelete(0, lblName); ObjectCreate(0, lblName, OBJ_TEXT, 0, t2, a2Price); ObjectSetString(0, lblName, OBJPROP_TEXT, " " + trend); ObjectSetInteger(0, lblName, OBJPROP_COLOR, trendColor); ObjectSetInteger(0, lblName, OBJPROP_FONTSIZE, 10); ObjectSetString(0, lblName, OBJPROP_FONT, "Arial Bold"); ObjectSetInteger(0, lblName, OBJPROP_ANCHOR, ANCHOR_LEFT); ObjectSetInteger(0, lblName, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, lblName, OBJPROP_HIDDEN, true); ChartRedraw(); } g_ai_signalRequestInFlight = false; } //+------------------------------------------------------------------+ //| Key Level action - support or resistance level near price | //+------------------------------------------------------------------+ void AiKeyLevel() { //--- Throttle concurrent requests if(g_ai_signalRequestInFlight) { Ai_ShowToast("Signal in progress - please wait", true); return; } g_ai_signalRequestInFlight = true; //--- Configure bar count and build bars block const int N = 50; string barsBlock = Ai_FormatBarsDesc(Period(), N); if(StringLen(barsBlock) == 0) { g_ai_signalRequestInFlight = false; Ai_ShowToast("Key Level: failed to load bars", true); return; } //--- Read current price double currentPrice = SymbolInfoDouble(Symbol(), SYMBOL_BID); //--- Build key level prompt string prompt = "You are a precise support/resistance analyst.\n" "\n" + Ai_BuildBarPreamble() + "CURRENT PRICE (bid): " + DoubleToString(currentPrice, _Digits) + "\n" "\n" "DECISION RULES FOR KEY LEVEL:\n" " - Identify ONE significant horizontal level near current price (within reasonable trading distance).\n" " - LEVEL_TYPE is SUPPORT if the level is BELOW current price (price has bounced UP from there).\n" " - LEVEL_TYPE is RESISTANCE if the level is ABOVE current price (price has bounced DOWN from there).\n" " - Choose the level with strongest evidence: multiple touches, prior swing high/low, recent rejection.\n" " - PRICE must come from actual high/low values in the DATA - do not invent prices.\n" " - You MUST scan ALL 50 bars to identify the MOST SIGNIFICANT level - do not pick the first level you notice.\n" "\n" "TRADE BIAS - your read on what price will do AT this level:\n" " - TRADE_BIAS is BOUNCE if you expect price to reject at this level and reverse (level holds).\n" " Use BOUNCE when the level has been repeatedly defended with strong rejections.\n" " - TRADE_BIAS is BREAK if you expect price to push through this level (level fails).\n" " Use BREAK when price is approaching the level with strong momentum, multiple recent failed defenses, or the level has weakened.\n" "\n" "REASON FORMATTING - REQUIRED:\n" " Your REASON must include ALL of the following, in this order:\n" " (a) State how many bars you analyzed (always 50 for this action).\n" " (b) State the OVERALL price range - highest HIGH and lowest LOW across the set, with their bar numbers.\n" " (c) Cite ALL the bars where price touched/tested this level (with their high or low values).\n" " (d) Explain why you chose THIS level over other candidates you saw.\n" " (e) Justify the TRADE_BIAS based on the most recent test(s) - what did price do the last 1-2 times it reached this level?\n" "\n" "OUTPUT FORMAT (LINE-BASED KEY: VALUE - NO JSON, NO MARKDOWN, NO PROSE):\n" "Output EXACTLY 4 lines, in this order:\n" "LEVEL_TYPE: \n" "PRICE: \n" "TRADE_BIAS: \n" "REASON: \n" "\n" "EXAMPLE (replace values with actual ones from DATA - note how it covers ALL 50 bars and multiple touches):\n" "LEVEL_TYPE: RESISTANCE\n" "PRICE: 1.10500\n" "TRADE_BIAS: BOUNCE\n" "REASON: Analyzed 50 bars; range was 1.10080 (low at Bar 12) to 1.10545 (high at Bar 41); level 1.10500 was tested at Bars 41 (H=1.10545), 28 (H=1.10510), 18 (H=1.10495), 8 (H=1.10500) - four touches across the set; chose this over a weaker level near 1.10300 because 1.10500 has 4 clean rejections vs only 2 partial touches at 1.10300; bias BOUNCE because the most recent two tests at Bars 8 and 18 both produced strong upper wicks > 30 pips and immediate reversal - level remains well-defended\n" "\n" "DATA:\n" + barsBlock; //--- Time the call ulong t0 = GetTickCount(); string resp = AiGetChatGPTResponse(prompt, 0.2); ulong elapsed = GetTickCount() - t0; //--- Parse response string levelType = Ai_ParseKVResponse(resp, "LEVEL_TYPE"); string priceStr = Ai_ParseKVResponse(resp, "PRICE"); string tradeBias = Ai_ParseKVResponse(resp, "TRADE_BIAS"); string reason = Ai_ParseKVResponse(resp, "REASON"); StringToUpper(levelType); StringToUpper(tradeBias); double price = StringToDouble(priceStr); //--- Validate level type and price relationship bool valid = (price > 0 && (levelType == "SUPPORT" || levelType == "RESISTANCE")); if(valid) { if(price < currentPrice && levelType != "SUPPORT") levelType = "SUPPORT"; if(price > currentPrice && levelType != "RESISTANCE") levelType = "RESISTANCE"; } //--- Default trade bias if invalid if(tradeBias != "BOUNCE" && tradeBias != "BREAK") tradeBias = "BOUNCE"; //--- Build chat bubble string bubble; if(valid) bubble = "Key Level -> " + levelType + " at " + DoubleToString(price, _Digits) + " | Bias: " + tradeBias + "\n" + reason; else bubble = "Key Level -> no clear level found\n" + reason; //--- Auto-trade pending order based on level type and bias if(AutoTrade && valid) { ENUM_ORDER_TYPE ot = (ENUM_ORDER_TYPE)-1; double sl = 0.0, tp = 0.0; const double buf = Ai_DynamicBuffer(); //--- SUPPORT + BOUNCE -> BUY LIMIT if(levelType == "SUPPORT" && tradeBias == "BOUNCE") { ot = ORDER_TYPE_BUY_LIMIT; sl = price - buf; tp = price + 2.0 * (price - sl); } //--- SUPPORT + BREAK -> SELL STOP else if(levelType == "SUPPORT" && tradeBias == "BREAK") { ot = ORDER_TYPE_SELL_STOP; sl = price + buf; tp = price - 2.0 * (sl - price); } //--- RESISTANCE + BOUNCE -> SELL LIMIT else if(levelType == "RESISTANCE" && tradeBias == "BOUNCE") { ot = ORDER_TYPE_SELL_LIMIT; sl = price + buf; tp = price - 2.0 * (sl - price); } //--- RESISTANCE + BREAK -> BUY STOP else if(levelType == "RESISTANCE" && tradeBias == "BREAK") { ot = ORDER_TYPE_BUY_STOP; sl = price - buf; tp = price + 2.0 * (price - sl); } //--- Submit pending order if(ot != (ENUM_ORDER_TYPE)-1) { bool ok = false; ulong tk = 0; string err = ""; Ai_PlaceOrder(ot, price, sl, tp, "Key Level " + levelType + "/" + tradeBias, ok, tk, err); if(ok) { bubble += "\nTrade: " + Ai_OrderTypeLabel(ot) + " @ " + DoubleToString(price, _Digits) + " | SL=" + DoubleToString(sl, _Digits) + " | TP=" + DoubleToString(tp, _Digits) + " | Ticket #" + IntegerToString((long)tk); } else { bubble += "\nTrade FAILED: " + err; Ai_ShowToast("Key Level trade failed: " + err, true); } } } //--- Push to chat history Ai_PushSignalActionToChat("Key Level", bubble, elapsed); //--- Draw horizontal level and label if(valid) { static int s_levelSeq = 0; s_levelSeq++; color levelColor = (levelType == "SUPPORT") ? clrMediumSeaGreen : clrIndianRed; //--- Create horizontal line string lineName = AI_SIGNAL_OBJ_PREFIX + "KeyLevel_HLine_" + IntegerToString(s_levelSeq); if(ObjectFind(0, lineName) >= 0) ObjectDelete(0, lineName); ObjectCreate(0, lineName, OBJ_HLINE, 0, 0, price); ObjectSetInteger(0, lineName, OBJPROP_COLOR, levelColor); ObjectSetInteger(0, lineName, OBJPROP_WIDTH, 1); ObjectSetInteger(0, lineName, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(0, lineName, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, lineName, OBJPROP_HIDDEN, true); //--- Create label anchored to last closed bar datetime tNow = iTime(Symbol(), Period(), 1); string lblName = AI_SIGNAL_OBJ_PREFIX + "KeyLevel_Label_" + IntegerToString(s_levelSeq); if(ObjectFind(0, lblName) >= 0) ObjectDelete(0, lblName); ObjectCreate(0, lblName, OBJ_TEXT, 0, tNow, price); ObjectSetString(0, lblName, OBJPROP_TEXT, " " + levelType + " " + DoubleToString(price, _Digits)); ObjectSetInteger(0, lblName, OBJPROP_COLOR, levelColor); ObjectSetInteger(0, lblName, OBJPROP_FONTSIZE, 9); ObjectSetString(0, lblName, OBJPROP_FONT, "Arial Bold"); ObjectSetInteger(0, lblName, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); ObjectSetInteger(0, lblName, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, lblName, OBJPROP_HIDDEN, true); ChartRedraw(); } g_ai_signalRequestInFlight = false; } //+------------------------------------------------------------------+ //| Clear all AI signal drawings from chart | //+------------------------------------------------------------------+ void AiClearSignalDrawings() { //--- Count objects before deletion const int before = ObjectsTotal(0, -1, -1); //--- Delete all objects matching prefix ObjectsDeleteAll(0, AI_SIGNAL_OBJ_PREFIX); //--- Count objects after and compute removed const int after = ObjectsTotal(0, -1, -1); const int removed = before - after; ChartRedraw(); //--- Notify user via toast if(removed > 0) { Ai_ShowToast("Cleared " + IntegerToString(removed) + " signal drawing(s)", false); } else { Ai_ShowToast("No signal drawings to clear", false); } } //+------------------------------------------------------------------+ //| Quick Scalp action - candlestick pattern signal | //+------------------------------------------------------------------+ void AiGetTradeSignal(bool isAuto = false) { //--- Throttle concurrent requests if(!isAuto && g_ai_signalRequestInFlight) { Ai_ShowToast("Signal in progress - please wait", true); return; } g_ai_signalRequestInFlight = true; //--- Build mode label and log start string modeStr = isAuto ? " (Auto-mode)" : " (Manual)"; Print("Starting Quick Scalp signal analysis", modeStr); //--- Pull bars and order newest first const int N = 10; MqlRates ra[]; if(CopyRates(Symbol(), Period(), 1, N, ra) < N) { g_ai_signalRequestInFlight = false; Alert("Quick Scalp: failed to load " + IntegerToString(N) + " bars."); return; } ArraySetAsSeries(ra, true); //--- Build bars block string barsBlock = Ai_FormatBarsDesc(Period(), N); if(StringLen(barsBlock) == 0) { g_ai_signalRequestInFlight = false; Alert("Quick Scalp: failed to format bars."); return; } //--- Build pattern-detection prompt string prompt = "You are a precise candlestick-pattern trading analyst. Analyze the last " + IntegerToString(N) + " bars in DATA.\n" "\n" + Ai_BuildBarPreamble() + "PATTERN DEFINITIONS (match exactly - if no exact match, output NONE):\n" " - BULLISH ENGULFING: a BULLISH bar whose body fully engulfs the prior bar's body, AND the prior bar is BEARISH.\n" " - BEARISH ENGULFING: a BEARISH bar whose body fully engulfs the prior bar's body, AND the prior bar is BULLISH.\n" " - HAMMER: a BULLISH bar with a small body near the top and a long lower wick (lower wick >= 2x body).\n" " - SHOOTING STAR: a BEARISH bar with a small body near the bottom and a long upper wick (upper wick >= 2x body).\n" " - PIN BAR (BULLISH): long lower wick rejecting lower prices; close in upper third of bar range.\n" " - PIN BAR (BEARISH): long upper wick rejecting higher prices; close in lower third of bar range.\n" " - INSIDE BAR: a bar whose high <= prior high AND low >= prior low (entirely inside prior bar's range).\n" "\n" "DECISION RULES:\n" " - If you find a clear BULLISH pattern (BULLISH ENGULFING, HAMMER, BULLISH PIN BAR) on Bar 1 or Bar 2 -> SIGNAL is BUY.\n" " - If you find a clear BEARISH pattern (BEARISH ENGULFING, SHOOTING STAR, BEARISH PIN BAR) on Bar 1 or Bar 2 -> SIGNAL is SELL.\n" " - INSIDE BAR can be either direction depending on context.\n" " - If no clear pattern matches the definitions above -> SIGNAL is NONE.\n" "\n" "PRICE GUIDANCE (these MUST come from actual OHLC values in the DATA):\n" " - ENTRY = the TRIGGER_BAR close price.\n" " - SL (stop loss): for BUY, just BELOW the trigger bar's LOW (e.g. trigger LOW minus a few pips). For SELL, just ABOVE the trigger bar's HIGH.\n" " - TP (take profit): roughly 2x the SL distance away from ENTRY in the trade direction (so target reward = 2x risk).\n" " - You MUST scan ALL 10 bars before settling on a trigger - pick the STRONGEST pattern, not the first one you spot.\n" "\n" "REASON FORMATTING - REQUIRED:\n" " Your REASON must include ALL of the following, in this order:\n" " (a) State how many bars you analyzed (always 10 for this action).\n" " (b) State the price range across the 10 bars (highest HIGH, lowest LOW with bar numbers).\n" " (c) Name the EXACT pattern at the TRIGGER_BAR with its OHLC values, and the prior bar's OHLC if relevant (e.g. for engulfing).\n" " (d) Briefly state why this pattern over alternatives - were there competing patterns at other bars? If yes, why this one wins.\n" "\n" "OUTPUT FORMAT (LINE-BASED KEY: VALUE - NO JSON, NO MARKDOWN, NO PROSE):\n" "Output EXACTLY 6 lines, in this order:\n" "SIGNAL: \n" "REASON: \n" "TRIGGER_BAR: \n" "ENTRY: \n" "SL: \n" "TP: \n" "\n" "EXAMPLE (replace values with actual ones from DATA - note how REASON covers ALL 4 elements):\n" "SIGNAL: BUY\n" "REASON: Analyzed 10 bars; range 1.09980 (low at Bar 7) to 1.10310 (high at Bar 4); Bar 1 BULLISH ENGULFING - Bar 1 O=1.10100 C=1.10280 (BULLISH) engulfs Bar 2 O=1.10240 C=1.10110 (BEARISH), Bar 1 body fully covers Bar 2 body; chose this over a weaker hammer at Bar 5 because the engulfing has higher volume context (Bar 1 range 180 pips vs Bar 5 range 60 pips) and is at the most recent bar\n" "TRIGGER_BAR: 1\n" "ENTRY: 1.10280\n" "SL: 1.10080\n" "TP: 1.10680\n" "\n" "DATA:\n" + barsBlock; //--- Time the call ulong signalStartMs = GetTickCount(); string response = AiGetChatGPTResponse(prompt, 0.0); ulong signalElapsedMs = GetTickCount() - signalStartMs; int signalElapsedSec = (int)(signalElapsedMs / 1000); string signalTimeNote = "\n(Response in " + IntegerToString(signalElapsedSec) + "s)"; Print("Raw AI Response: ", response); //--- Parse response string signal = Ai_ParseKVResponse(response, "SIGNAL"); string reason = Ai_ParseKVResponse(response, "REASON"); string trigStr = Ai_ParseKVResponse(response, "TRIGGER_BAR"); string entryStr = Ai_ParseKVResponse(response, "ENTRY"); string slStr = Ai_ParseKVResponse(response, "SL"); string tpStr = Ai_ParseKVResponse(response, "TP"); StringToUpper(signal); //--- Validate signal if(signal != "BUY" && signal != "SELL" && signal != "NONE") { g_ai_signalRequestInFlight = false; Alert("Quick Scalp: invalid SIGNAL '" + signal + "'. Raw: " + response); return; } //--- Convert numeric fields int triggerBar = (int)StringToInteger(trigStr); double entry = StringToDouble(entryStr); double sl = StringToDouble(slStr); double tp = StringToDouble(tpStr); if(triggerBar < 1 || triggerBar > N) triggerBar = 1; //--- Apply local fallbacks for entry/SL/TP if(signal == "BUY" || signal == "SELL") { const MqlRates trig = ra[triggerBar - 1]; const double bufferPts = Ai_DynamicBuffer(); if(entry <= 0.0) entry = trig.close; if(signal == "BUY") { if(sl <= 0.0) sl = trig.low - bufferPts; if(tp <= 0.0) tp = entry + 2.0 * (entry - sl); } else { if(sl <= 0.0) sl = trig.high + bufferPts; if(tp <= 0.0) tp = entry - 2.0 * (sl - entry); } } //--- Build chat bubble string bubble; if(signal == "BUY" || signal == "SELL") { bubble = "Quick Scalp -> " + signal + " at " + DoubleToString(entry, _Digits) + " (SL=" + DoubleToString(sl, _Digits) + ", TP=" + DoubleToString(tp, _Digits) + ", Bar " + IntegerToString(triggerBar) + ")" + "\n" + reason; } else { bubble = "Quick Scalp -> NONE\n" + reason; } //--- Auto-trade if enabled if(AutoTrade && (signal == "BUY" || signal == "SELL")) { const ENUM_ORDER_TYPE ot = (signal == "BUY") ? ORDER_TYPE_BUY : ORDER_TYPE_SELL; bool ok = false; ulong tk = 0; string err = ""; Ai_PlaceOrder(ot, 0.0, sl, tp, "Quick Scalp", ok, tk, err); if(ok) { bubble += "\nTrade: " + Ai_OrderTypeLabel(ot) + " @ " + DoubleToString(entry, _Digits) + " | SL=" + DoubleToString(sl, _Digits) + " | TP=" + DoubleToString(tp, _Digits) + " | Ticket #" + IntegerToString((long)tk); } else { bubble += "\nTrade FAILED: " + err; Ai_ShowToast("Quick Scalp trade failed: " + err, true); } } //--- Append to history string timestamp = TimeToString(TimeCurrent(), TIME_MINUTES); g_ai_conversationHistory += "You: Quick Scalp" + modeStr + "\n" + timestamp + "\n\n" + "AI: " + bubble + signalTimeNote + "\n" + timestamp + "\n\n"; g_ai_chatScrollPin = true; //--- Auto-rename placeholder title if(StringFind(g_ai_currentTitle, "Chat ") == 0) { g_ai_currentTitle = "Quick Scalp"; } //--- Persist and refresh AiUpdateCurrentHistory(); Ai_RenderAll(); ChartRedraw(); //--- Visualize signal on chart string fullReason = reason + " (Bar " + IntegerToString(triggerBar) + ")"; if(signal != "NONE") AiVisualizeSignal(signal, fullReason, triggerBar, entry, sl, tp); else Print("Quick Scalp: NONE - skipping viz/trade."); g_ai_signalRequestInFlight = false; } //+------------------------------------------------------------------+ //| Append chart data to current prompt | //+------------------------------------------------------------------+ void AiGetAndAppendChartData() { //--- Pull chart data string string data = AiGetChartDataString(); if(StringLen(data) == 0) return; //--- Log data Print("Chart data appended to prompt: \n", data); if(g_ai_logFileHandle != INVALID_HANDLE) FileWrite(g_ai_logFileHandle, "Chart data appended to prompt: \n" + data); //--- Append to current prompt with newline separator if(StringLen(g_ai_currentPrompt) > 0) g_ai_currentPrompt += "\n"; g_ai_currentPrompt += data; //--- Sync editor and refresh g_ai_editor.SetText(g_ai_currentPrompt); Ai_RenderAll(); ChartRedraw(); } //+------------------------------------------------------------------+ //| Submit user message to AI and update chat | //+------------------------------------------------------------------+ void AiSubmitMessage(string prompt) { //--- Strip surrounding blank lines prompt = AiStripLeadingBlankLines(prompt); prompt = AiStripTrailingBlankLines(prompt); if(StringLen(prompt) == 0) return; //--- Build timestamp and init response state string timestamp = TimeToString(TimeCurrent(), TIME_MINUTES); string response = ""; bool send_to_api = true; //--- Handle local "set title" command if(StringFind(prompt, "set title ") == 0) { string new_title = StringSubstr(prompt, 10); g_ai_currentTitle = new_title; response = "Title set to " + new_title; send_to_api = false; AiUpdateCurrentHistory(); } //--- Append user turn to history g_ai_conversationHistory += "You: " + prompt + "\n" + timestamp + "\n\n"; g_ai_chatScrollPin = true; //--- Process API path if(send_to_api) { //--- Append preparing placeholder g_ai_conversationHistory += AI_PREP_BASE + "\n" + timestamp + "\n\n"; g_ai_chatScrollPin = true; Ai_RenderAll(); ChartRedraw(); //--- Run pre-animation cycles for(int i = 0; i < AI_PRE_ANIM_CYCLES; i++) { int subCycle = i % 3; string dots = ""; for(int d = 0; d <= subCycle; d++) dots += "."; int prepPos = StringFind(g_ai_conversationHistory, AI_PREP_BASE, 0); if(prepPos >= 0) { int endPos = StringFind(g_ai_conversationHistory, "\n\n", prepPos) + 2; if(endPos < 2) endPos = StringLen(g_ai_conversationHistory); string before = StringSubstr(g_ai_conversationHistory, 0, prepPos); string after = StringSubstr(g_ai_conversationHistory, endPos); g_ai_conversationHistory = before + AI_PREP_BASE + dots + "\n" + timestamp + "\n\n" + after; } g_ai_chatScrollPin = true; Ai_RenderAll(); ChartRedraw(); Sleep(200); } //--- Swap preparing placeholder for thinking placeholder int prepPos = StringFind(g_ai_conversationHistory, AI_PREP_BASE, 0); if(prepPos >= 0) { int endPos = StringFind(g_ai_conversationHistory, "\n\n", prepPos) + 2; if(endPos < 2) endPos = StringLen(g_ai_conversationHistory); string before = StringSubstr(g_ai_conversationHistory, 0, prepPos); string after = StringSubstr(g_ai_conversationHistory, endPos); g_ai_conversationHistory = before + AI_LOADING_PLACEHOLDER + "\n" + timestamp + "\n\n" + after; } else { g_ai_conversationHistory += AI_LOADING_PLACEHOLDER + "\n" + timestamp + "\n\n"; } g_ai_chatScrollPin = true; Ai_RenderAll(); ChartRedraw(); //--- Issue API request g_ai_startTimeMs = GetTickCount(); Print("Chat ID: ", g_ai_currentChatId, ", Title: ", g_ai_currentTitle); if(g_ai_logFileHandle != INVALID_HANDLE) FileWrite(g_ai_logFileHandle, "Chat ID: " + IntegerToString(g_ai_currentChatId) + ", Title: " + g_ai_currentTitle); Print("User: ", prompt); if(g_ai_logFileHandle != INVALID_HANDLE) FileWrite(g_ai_logFileHandle, "User: " + prompt); response = AiGetChatGPTResponse(prompt); Print("AI: ", response); if(g_ai_logFileHandle != INVALID_HANDLE) FileWrite(g_ai_logFileHandle, "AI: " + response); //--- Compute elapsed time note ulong elapsedMs = GetTickCount() - g_ai_startTimeMs; int elapsedSec = (int)(elapsedMs / 1000); string timeNote = "\n(Response in " + IntegerToString(elapsedSec) + "s)"; //--- Replace thinking placeholder with real response int placeholderPos = StringFind(g_ai_conversationHistory, AI_LOADING_PLACEHOLDER, 0); if(placeholderPos >= 0) { int endPos = StringFind(g_ai_conversationHistory, "\n\n", placeholderPos) + 2; if(endPos < 2) endPos = StringLen(g_ai_conversationHistory); string before = StringSubstr(g_ai_conversationHistory, 0, placeholderPos); string after = StringSubstr(g_ai_conversationHistory, endPos); g_ai_conversationHistory = before + "AI: " + response + timeNote + "\n" + timestamp + "\n\n" + after; } else { g_ai_conversationHistory += "AI: " + response + timeNote + "\n" + timestamp + "\n\n"; } //--- Auto-truncate placeholder title from prompt bool didAutoTruncate = false; if(StringFind(g_ai_currentTitle, "Chat ") == 0) { g_ai_currentTitle = StringSubstr(prompt, 0, 30); if(StringLen(prompt) > 30) g_ai_currentTitle += "..."; AiUpdateCurrentHistory(); didAutoTruncate = true; } //--- Auto-name via secondary AI call on first exchange only if(didAutoTruncate) { int aiTurnCount = 0; int searchPos = 0; while(true) { int found = StringFind(g_ai_conversationHistory, "AI: ", searchPos); if(found < 0) break; aiTurnCount++; searchPos = found + 4; } if(aiTurnCount == 1) { Ai_AutoTitleChat(prompt, response); } } } else { //--- Append local response without API call g_ai_conversationHistory += "AI: " + response + "\n" + timestamp + "\n\n"; } //--- Persist and refresh AiUpdateCurrentHistory(); Ai_RenderAll(); ChartRedraw(); } #endif // AI_LOGIC_MQH