312 lines
13 KiB
MQL5
312 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
| |||
//+------------------------------------------------------------------+
|