2476 lines
No EOL
99 KiB
MQL5
2476 lines
No EOL
99 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 <Trade/Trade.mqh>
|
|
#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: <BUY or SELL or NONE>\n"
|
|
"REASON: Bar 1 O=<bar1_open> C=<bar1_close> -> <BULLISH/BEARISH/DOJI>; Bar 2 O=<bar2_open> C=<bar2_close> -> <BULLISH/BEARISH/DOJI>; Decision: <both BULLISH=>BUY | both BEARISH=>SELL | mixed=>NONE>\n"
|
|
"ENTRY: <decimal price equal to Bar 1 close, with 5 decimals; or 0 if SIGNAL is NONE>\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: <BUY or SELL or NONE>\n"
|
|
"REASON: <single-line summary covering all 5 elements (a)-(e) above, separated by '; '>\n"
|
|
"ENTRY: <decimal price equal to Bar 1 close; or 0 if SIGNAL is NONE>\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: <UP or DOWN or RANGE>\n"
|
|
"ANCHOR1_BAR: <integer 1..30>\n"
|
|
"ANCHOR1_PRICE: <decimal price from that bar>\n"
|
|
"ANCHOR2_BAR: <integer 1..30, must be smaller than ANCHOR1_BAR for chronological order>\n"
|
|
"ANCHOR2_PRICE: <decimal price from that bar>\n"
|
|
"REASON: <single-line summary covering all 5 elements (a)-(e), separated by '; '>\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: <SUPPORT or RESISTANCE>\n"
|
|
"PRICE: <decimal price, 5 decimals, an actual high or low from the bars>\n"
|
|
"TRADE_BIAS: <BOUNCE or BREAK>\n"
|
|
"REASON: <single-line summary covering all 5 elements (a)-(e), separated by '; '>\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: <BUY or SELL or NONE>\n"
|
|
"REASON: <single-line summary covering all 4 elements (a)-(d), separated by '; '>\n"
|
|
"TRIGGER_BAR: <integer 1..10, the bar where the pattern formed>\n"
|
|
"ENTRY: <decimal price, 5 decimals; or 0 if SIGNAL is NONE>\n"
|
|
"SL: <decimal price, 5 decimals; or 0 if SIGNAL is NONE>\n"
|
|
"TP: <decimal price, 5 decimals; or 0 if SIGNAL is NONE>\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 |