308 lines
13 KiB
MQL5
308 lines
13 KiB
MQL5
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| AI Canvas State.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_CANVAS_STATE_MQH
|
||
|
|
#define AI_CANVAS_STATE_MQH
|
||
|
|
|
||
|
|
//--- Pull theme for AI_DASHBOARD_X_DEFAULT and AI_DASHBOARD_Y_DEFAULT
|
||
|
|
#include "AI Canvas Theme.mqh"
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Canvas Object Names |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
#define AI_CANVAS_NAME_MAIN "ChatGPT_AI_MainCanvas" // Main dashboard canvas object
|
||
|
|
#define AI_CANVAS_NAME_PROMPT "ChatGPT_AI_PromptCanvas" // Prompt overlay canvas object
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Chat Record Structure |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
struct Chat
|
||
|
|
{
|
||
|
|
int id; // Unique chat identifier
|
||
|
|
string title; // Chat display title
|
||
|
|
string history; // Full conversation transcript
|
||
|
|
};
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Chat State |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
Chat g_ai_chats[]; // All loaded chats
|
||
|
|
int g_ai_currentChatId = -1; // Active chat id (-1 = new chat)
|
||
|
|
string g_ai_currentTitle = ""; // Active chat title
|
||
|
|
string g_ai_conversationHistory = ""; // Active chat transcript
|
||
|
|
string g_ai_currentPrompt = ""; // Persisted prompt text
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| UI Visibility Flags |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool g_ai_sidebarExpanded = true; // Sidebar expanded state
|
||
|
|
bool g_ai_dashboardVisible = true; // Dashboard visible state
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Popup State Flags |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool g_ai_showSmallHistory = false; // Small history popup visible
|
||
|
|
bool g_ai_showBigHistory = false; // Big history popup visible
|
||
|
|
bool g_ai_showSearch = false; // Search popup visible
|
||
|
|
bool g_ai_justOpenedSmall = false; // Skip first-click close on small popup
|
||
|
|
bool g_ai_justOpenedBig = false; // Skip first-click close on big popup
|
||
|
|
bool g_ai_justOpenedSearch = false; // Skip first-click close on search popup
|
||
|
|
string g_ai_searchQuery = ""; // Current search query text
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Drag State |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool g_ai_dragging = false; // Header drag in progress
|
||
|
|
int g_ai_dragOffsetX = 0; // Drag X offset from anchor
|
||
|
|
int g_ai_dragOffsetY = 0; // Drag Y offset from anchor
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Dashboard Position |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int AI_DASHBOARD_X = AI_DASHBOARD_X_DEFAULT; // Current dashboard X
|
||
|
|
int AI_DASHBOARD_Y = AI_DASHBOARD_Y_DEFAULT; // Current dashboard Y
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Footer Dropdown State |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool g_ai_showFooterDropdown = false; // Dropdown popup visible
|
||
|
|
int g_ai_footerDropdownSelectedIdx = 0; // Selected action index
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Footer Action Dispatch Tables |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
const int AI_FOOTER_DD_COUNT = 7; // Number of dropdown actions
|
||
|
|
const string AI_FOOTER_DD_LABELS[] = {
|
||
|
|
"Get Chart Data", // 0 - chart data dump
|
||
|
|
"Twin Bars", // 1 - twin-bars signal check
|
||
|
|
"Quick Scalp", // 2 - single-bar entry signal
|
||
|
|
"Daily Signal", // 3 - daily H1 bias signal
|
||
|
|
"Trend Read", // 4 - trendline anchors
|
||
|
|
"Key Level", // 5 - significant S/R level
|
||
|
|
"Clear Drawings" // 6 - wipe signal drawings
|
||
|
|
};
|
||
|
|
const int AI_FOOTER_DD_ACTION_IDS[]= { 0, 1, 2, 3, 4, 5, 6 }; // Stable action IDs
|
||
|
|
const int AI_FOOTER_DD_ICONS[] = {
|
||
|
|
0, // chart icon
|
||
|
|
1, // twin bars icon
|
||
|
|
2, // lightning icon
|
||
|
|
3, // day icon
|
||
|
|
4, // trend icon
|
||
|
|
5, // level icon
|
||
|
|
6 // close icon
|
||
|
|
};
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| API Throttle Flag |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool g_ai_signalRequestInFlight = false; // Block parallel API calls
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Tick and Animation State |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
datetime g_ai_lastBarTime = 0; // Last seen bar time (for new-bar detection)
|
||
|
|
int g_ai_spinnerCycle = 0; // Loading spinner animation tick
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Toast Notification State |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
string g_ai_toastText = ""; // Active toast text (empty = no toast)
|
||
|
|
bool g_ai_toastIsError = false;// Pick error vs success color
|
||
|
|
ulong g_ai_toastExpiryMs = 0; // Tick count when toast expires
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Pencil Hover Tracking |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool g_ai_overPencilIcon = false; // Cursor over narrow pencil icon
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Send Button State Tracking |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool g_ai_lastRenderedSendDisabled = true; // Disabled state at last footer render
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Encode integer ID as base62 string |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
string AiEncodeID(int id)
|
||
|
|
{
|
||
|
|
//--- Setup base62 alphabet
|
||
|
|
string chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||
|
|
string res = "";
|
||
|
|
|
||
|
|
//--- Special-case zero
|
||
|
|
if(id == 0) return "0";
|
||
|
|
|
||
|
|
//--- Build digits from least significant
|
||
|
|
while(id > 0)
|
||
|
|
{
|
||
|
|
res = StringSubstr(chars, id % 62, 1) + res;
|
||
|
|
id /= 62;
|
||
|
|
}
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Decode base62 string back to integer ID |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int AiDecodeID(string enc)
|
||
|
|
{
|
||
|
|
//--- Setup base62 alphabet
|
||
|
|
string chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||
|
|
int id = 0;
|
||
|
|
|
||
|
|
//--- Accumulate digits from most significant
|
||
|
|
for(int i = 0; i < StringLen(enc); i++)
|
||
|
|
{
|
||
|
|
id = id * 62 + StringFind(chars, StringSubstr(enc, i, 1));
|
||
|
|
}
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Find chat array index by chat ID |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int AiGetChatIndex(int id)
|
||
|
|
{
|
||
|
|
//--- Linear scan for matching id
|
||
|
|
for(int i = 0; i < ArraySize(g_ai_chats); i++)
|
||
|
|
{
|
||
|
|
if(g_ai_chats[i].id == id) return i;
|
||
|
|
}
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Test if line matches HH:MM timestamp format |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
bool AiIsTimestamp(string line)
|
||
|
|
{
|
||
|
|
//--- Trim whitespace and check exact length
|
||
|
|
StringTrimLeft(line);
|
||
|
|
StringTrimRight(line);
|
||
|
|
if(StringLen(line) != 5) return false;
|
||
|
|
|
||
|
|
//--- Verify colon separator at position 2
|
||
|
|
if(StringGetCharacter(line, 2) != ':') return false;
|
||
|
|
|
||
|
|
//--- Parse and validate hour and minute fields
|
||
|
|
string hh = StringSubstr(line, 0, 2);
|
||
|
|
string mm = StringSubstr(line, 3, 2);
|
||
|
|
int h = (int)StringToInteger(hh);
|
||
|
|
int m = (int)StringToInteger(mm);
|
||
|
|
if(h < 0 || h > 23 || m < 0 || m > 59) return false;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Split string by delimiter into output array |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
int AiSplitOnString(string inputText, string delim, string &result[])
|
||
|
|
{
|
||
|
|
//--- Clear output array
|
||
|
|
ArrayResize(result, 0);
|
||
|
|
int pos = 0;
|
||
|
|
int delim_len = StringLen(delim);
|
||
|
|
|
||
|
|
//--- Walk delimiter occurrences
|
||
|
|
while(true)
|
||
|
|
{
|
||
|
|
int found = StringFind(inputText, delim, pos);
|
||
|
|
if(found == -1)
|
||
|
|
{
|
||
|
|
//--- Append final remainder
|
||
|
|
string part = StringSubstr(inputText, pos);
|
||
|
|
if(StringLen(part) > 0 || ArraySize(result) > 0)
|
||
|
|
{
|
||
|
|
int size = ArraySize(result);
|
||
|
|
ArrayResize(result, size + 1);
|
||
|
|
result[size] = part;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
//--- Append substring up to delimiter
|
||
|
|
string part = StringSubstr(inputText, pos, found - pos);
|
||
|
|
int size = ArraySize(result);
|
||
|
|
ArrayResize(result, size + 1);
|
||
|
|
result[size] = part;
|
||
|
|
pos = found + delim_len;
|
||
|
|
}
|
||
|
|
return ArraySize(result);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Darken a color by scaling each channel toward black |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
color AiDarkenColor(color colorValue, double factor = 0.8)
|
||
|
|
{
|
||
|
|
//--- Scale RGB channels by factor
|
||
|
|
int red = int(((colorValue ) & 0xFF) * factor);
|
||
|
|
int green = int(((colorValue >> 8) & 0xFF) * factor);
|
||
|
|
int blue = int(((colorValue >> 16) & 0xFF) * factor);
|
||
|
|
return (color)(red | (green << 8) | (blue << 16));
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Lighten a color by interpolating each channel toward white |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
color AiLightenColor(color colorValue, double amount = 0.25)
|
||
|
|
{
|
||
|
|
//--- Extract RGB channels
|
||
|
|
int r = (int)((colorValue ) & 0xFF);
|
||
|
|
int g = (int)((colorValue >> 8) & 0xFF);
|
||
|
|
int b = (int)((colorValue >> 16) & 0xFF);
|
||
|
|
|
||
|
|
//--- Interpolate each channel toward 255
|
||
|
|
r = r + (int)((255 - r) * amount);
|
||
|
|
g = g + (int)((255 - g) * amount);
|
||
|
|
b = b + (int)((255 - b) * amount);
|
||
|
|
|
||
|
|
//--- Clamp at 255
|
||
|
|
if(r > 255) r = 255; if(g > 255) g = 255; if(b > 255) b = 255;
|
||
|
|
return (color)(r | (g << 8) | (b << 16));
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Replace standalone double-periods with newlines |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
string AiReplaceExactDoublePeriods(string text)
|
||
|
|
{
|
||
|
|
//--- Build result by scanning input
|
||
|
|
string result = "";
|
||
|
|
int len = StringLen(text);
|
||
|
|
for(int i = 0; i < len; i++)
|
||
|
|
{
|
||
|
|
//--- Detect ".." pattern
|
||
|
|
if(i + 1 < len && StringGetCharacter(text, i) == '.' && StringGetCharacter(text, i + 1) == '.')
|
||
|
|
{
|
||
|
|
//--- Reject when surrounded by additional dots
|
||
|
|
bool preceded = (i > 0 && StringGetCharacter(text, i - 1) == '.');
|
||
|
|
bool followed = (i + 2 < len && StringGetCharacter(text, i + 2) == '.');
|
||
|
|
if(!preceded && !followed)
|
||
|
|
{
|
||
|
|
//--- Standalone ".." converts to newline
|
||
|
|
result += "\n";
|
||
|
|
i++;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
//--- Part of longer dot run - keep literal
|
||
|
|
result += ".";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
//--- Copy character through unchanged
|
||
|
|
result += StringSubstr(text, i, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif // AI_CANVAS_STATE_MQH
|