tiltnote-toolkit/Tiltnote_Toolkit.mq5
2060 lines
100 KiB
MQL5
//+------------------------------------------------------------------+
//| Tiltnote_Toolkit.mq5 |
//| Tiltnote Toolkit (MT5 indicator) |
//+------------------------------------------------------------------+
// MIT License
//
// Copyright (c) 2026 Tiltnote Okumura
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Display-only tool: it never places, closes or modifies orders. It draws lines and a small panel on the chart.
// No DLL, no WebRequest. All numbers are estimates calculated from the lines on the chart.
//
// How to use:
// - Gray line = entry (follows the price until you drag it; then it stays, like a limit order)
// - Red line = stop. Drag it and the lot size is recalculated. Below entry = long, above = short
// - Blue line = target (optional, panel button)
// - Panel: drag the "::" grip to move it. Fold, text size and risk % are changed on the panel and remembered
// - "More": margin, period record, spread and bar timer, calculation basis, notes
//
// MetaTrader 4 and 5 (MT4/MT5) are trademarks of MetaQuotes Ltd. This indicator is not affiliated with MetaQuotes.
//+------------------------------------------------------------------+
#property copyright "Copyright (c) 2026 Tiltnote Okumura (MIT License)"
#property version "2.00"
#property description "Display-only tool. Draws entry / stop / optional target lines on the chart and shows lot size, margin, period record, spread and bar timer."
#property description "It never places, closes or modifies orders. All numbers are estimates calculated from the lines on the chart."
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0
// Choices --------------------------------------------------
enum ENUM_TK_LANG
{
TK_LANG_AUTO = 0, // Auto (terminal language)
TK_LANG_JA = 1, // Japanese
TK_LANG_EN = 2 // English
};
// panel background
enum ENUM_TK_BG
{
TK_BG_SOLID = 0, // Opaque
TK_BG_LIGHT = 1, // Slightly see-through
TK_BG_HALF = 2, // More see-through
TK_BG_NONE = 3 // Text only, no background
};
enum ENUM_TK_RISK_MODE
{
TK_RISK_PERCENT = 0, // Percent of base (%)
TK_RISK_MONEY = 1 // Fixed amount
};
enum ENUM_TK_BASE
{
TK_BASE_BALANCE = 0, // Balance
TK_BASE_EQUITY = 1 // Equity
};
enum ENUM_TK_GOLDPIP
{
TK_GOLDPIP_01 = 0, // 1 pip = 0.1
TK_GOLDPIP_001 = 1, // 1 pip = 0.01
TK_GOLDPIP_1 = 2 // 1 pip = 1.0
};
enum ENUM_TK_CONTRACT
{
TK_CONTRACT_AUTO = 0, // Auto (broker setting)
TK_CONTRACT_1K = 1000, // 1,000
TK_CONTRACT_10K = 10000, // 10,000
TK_CONTRACT_100K = 100000, // 100,000
TK_CONTRACT_CUSTOM = 1 // Custom (below)
};
enum ENUM_TK_INITSTOP
{
TK_INIT_AUTO = 0, // Auto: ATR(14), rounded (FX at least 10 pips)
TK_INIT_PIPS = 1, // Fixed pips
TK_INIT_ATR = 2 // ATR(14) x multiplier, not rounded
};
enum ENUM_TK_PERIOD
{
TK_PERIOD_DAY = 0, // 1 day
TK_PERIOD_WEEK = 1, // 1 week (from Monday)
TK_PERIOD_MONTH = 2 // 1 month (from the 1st)
};
enum ENUM_TK_BOUNDARY
{
TK_BOUND_NY1700 = 0, // New York 17:00 (default, DST auto) / NY 17:00
TK_BOUND_SERVER = 1, // Broker server 00:00
TK_BOUND_JST = 2 // Custom hour (Japan time)
};
enum ENUM_TK_SCOPE
{
TK_SCOPE_SYMBOL = 0, // This symbol only
TK_SCOPE_ALL = 1 // All symbols
};
//--- Inputs (field names are in English, with the Japanese name in parentheses) ---------------------
input group "Display (表示)"
input ENUM_TK_LANG InpLanguage = TK_LANG_AUTO; // Language
input ENUM_BASE_CORNER InpCorner = CORNER_RIGHT_UPPER; // First panel corner
input int InpFontSize = 9; // Font size
input int InpOffsetX = 10; // Distance from the side, px
input int InpOffsetY = 25; // Distance from top/bottom, px
input color InpTextColor = clrNONE; // Text color (none = auto)
input color InpPanelColor = clrNONE; // Panel color (none = auto)
input ENUM_TK_BG InpBackground = TK_BG_SOLID; // Panel background
input group "Lot size (ロット計算)"
input ENUM_TK_RISK_MODE InpRiskMode = TK_RISK_PERCENT; // Risk per trade as
input double InpRiskPercent = 1.0; // Risk percent %, first value
input double InpRiskMoney = 0.0; // Risk amount, account currency
input ENUM_TK_BASE InpRiskBase = TK_BASE_BALANCE; // Base for percent
input ENUM_TK_INITSTOP InpInitStop = TK_INIT_AUTO; // First stop line distance
input double InpStopPips = 20.0; // Fixed pips / no ATR data
input double InpAtrMult = 1.0; // ATR multiplier
input ENUM_TK_GOLDPIP InpGoldPip = TK_GOLDPIP_01; // Gold: size of 1 pip
input double InpPipSize = 0.0; // Pip size override, 0 = auto
input ENUM_TK_CONTRACT InpContract = TK_CONTRACT_AUTO; // Units per 1 lot, FX only
input double InpContractCustom = 100000; // Custom units per lot
input group "More (詳しく の中身)"
input bool InpShowMargin = true; // Margin and margin level
input bool InpShowPeriod = true; // Period record
input bool InpShowSpread = true; // Spread and bar countdown
input group "Period record (期間の記録)"
input ENUM_TK_PERIOD InpPeriod = TK_PERIOD_DAY; // Period
input ENUM_TK_BOUNDARY InpBoundary = TK_BOUND_NY1700; // Period boundary
input int InpJstHour = 7; // Custom hour, Japan time 0-23
input ENUM_TK_SCOPE InpScope = TK_SCOPE_ALL; // Symbols
input double InpLimitLoss = 0.0; // Loss limit for the period, 0 = off
input int InpLimitTrades = 0; // Trade count limit, 0 = off
// Internal state ------------------------------------------
// object name prefix (fixed)
string TK_PFX = "TKTA_";
string TK_PANEL = "TKTA_P"; // all panel objects start with this
string TK_RR_ENTRY = "TKTA_RR_ENTRY";
string TK_RR_SL = "TKTA_RR_SL";
string TK_RR_TP = "TKTA_RR_TP";
string TK_RR_SYM = "TKTA_RR_SYM";
int g_state = 0; // 0 = waiting, 1 = running, 2 = duplicate copy (does nothing)
double g_instId = 0; // this instance's number (so a copy never mistakes another copy's marker for its own)
uint g_waitFrom = 0; // when waiting started
// marker for a copy running on the same chart (terminal-wide global variable, one per chart ID)
// "Running on this chart" markers: terminal global variables per chart ID
// TKTA_RUN_<chart> = last heartbeat time (updated every second)
// instance number of the running copy
string TkRunName() { return "TKTA_RUN_" + IntegerToString(ChartID()); }
string TkIdName() { return "TKTA_ID_" + IntegerToString(ChartID()); }
string TkForeName() { return "TKTA_FG_" + IntegerToString(ChartID()); } // original "Chart on foreground" setting
string TkScrollName() { return "TKTA_MS_" + IntegerToString(ChartID()); } // original "Scroll with mouse" setting
string TkMouseName() { return "TKTA_MM_" + IntegerToString(ChartID()); } // original mouse-move events
string TkMovedName() { return "TKTA_MV_" + IntegerToString(ChartID()); } // which lines were dragged
// is another copy refreshing the marker?
bool TkOtherRunning()
{
if(!GlobalVariableCheck(TkRunName()) || !GlobalVariableCheck(TkIdName())) return false;
if(GlobalVariableGet(TkIdName()) == g_instId) return false; // my own marker
return (MathAbs((double)GetTickCount() - GlobalVariableGet(TkRunName())) < 3000.0);
}
string TkInstName() { return TkRunName(); }
void TkMarkRunning()
{
GlobalVariableSet(TkIdName(), g_instId);
GlobalVariableSet(TkRunName(), (double)GetTickCount());
}
// remove objects named by older builds
void TkRemoveOldNames()
{
ObjectsDeleteAll(0, "TKT_");
for(int n = 0; n < 20; n++) ObjectsDeleteAll(0, "TKT" + IntegerToString(n) + "_");
ObjectsDeleteAll(0, TK_PFX + "L"); // panel lines of 1.00
ObjectDelete(0, TK_PFX + "BG"); // panel background of 1.00
}
bool g_ja = true; // Japanese text?
double g_pip = 0.0; // pip size in price
long g_srvOffset = 0; // server time minus UTC (seconds)
datetime g_lastHist = 0; // last history scan (PC time)
// Period record results
double g_pNet = 0.0;
int g_pTrades = 0;
int g_pStreak = 0;
bool g_pOk = false;
datetime g_pStart = 0; // period start (server time)
bool g_pHistShort = false; // the history we could read starts in the middle of the period (MT4 Account History tab; always false on MT5) 2026-10-22
// loaded history starts after the period start (MT4 history tab; always false on MT5)
// Colors (set in OnInit)
color g_cText, g_cMuted, g_cHead, g_cWarn, g_cAlert, g_cPanel, g_cBorder, g_cBtn;
color g_cEntry = C'150,150,150', g_cSL = C'220,70,70', g_cTP = C'60,130,220';
color g_cEntryText = C'90,90,90'; // text next to the entry line (chosen by the background brightness)
color g_lastBg = clrNONE; // last seen chart background
string g_font = "Segoe UI";
string T(const string ja, const string en) { return g_ja ? ja : en; }
//--- Remembered settings (terminal-wide global variables; the same next time) ----
double g_riskPct = 1.0; // risk percent
bool g_fold = false; // folded
bool g_det = false; // details open
int g_fontLv = 2; // text size level 0-4 (2 = normal)
double g_rrSel = 0; // selected ratio (0 = none selected / moved by hand)
bool g_showTP = false; // show target line
int g_dir = 1; // chosen side: 1 = long, -1 = short (chosen with the buttons; 2026-10-22)
bool g_guideOff = false; // guide dismissed
int g_px = -1, g_py = -1; // panel top-left (-1 = from the first corner)
int g_mv = 0; // dragged lines
void TkLoadSettings()
{
double pct = InpRiskPercent;
// if the input changed, the input wins
if(GlobalVariableCheck("TKTA_S_RISK") && GlobalVariableCheck("TKTA_S_RISKIN") && GlobalVariableGet("TKTA_S_RISKIN") == InpRiskPercent)
pct = GlobalVariableGet("TKTA_S_RISK");
g_riskPct = MathMax(0.01, MathMin(10.0, pct));
g_fold = GlobalVariableCheck("TKTA_S_FOLD") && GlobalVariableGet("TKTA_S_FOLD") > 0.5;
g_det = GlobalVariableCheck("TKTA_S_DET") && GlobalVariableGet("TKTA_S_DET") > 0.5;
// text size level (old "big" → level 3)
if(GlobalVariableCheck("TKTA_S_FLV")) g_fontLv = (int)GlobalVariableGet("TKTA_S_FLV");
else g_fontLv = (GlobalVariableCheck("TKTA_S_BIG") && GlobalVariableGet("TKTA_S_BIG") > 0.5) ? 3 : 2;
g_fontLv = MathMax(0, MathMin(4, g_fontLv));
g_rrSel = GlobalVariableCheck("TKTA_S_RR") ? GlobalVariableGet("TKTA_S_RR") : 0;
g_showTP = GlobalVariableCheck("TKTA_S_TP") && GlobalVariableGet("TKTA_S_TP") > 0.5;
g_dir = (GlobalVariableCheck("TKTA_S_DIR") && GlobalVariableGet("TKTA_S_DIR") < 0) ? -1 : 1;
g_guideOff = GlobalVariableCheck("TKTA_S_GUIDE") && GlobalVariableGet("TKTA_S_GUIDE") > 0.5;
if(GlobalVariableCheck("TKTA_S_X") && GlobalVariableCheck("TKTA_S_Y"))
{
g_px = (int)GlobalVariableGet("TKTA_S_X");
g_py = (int)GlobalVariableGet("TKTA_S_Y");
}
g_mv = GlobalVariableCheck(TkMovedName()) ? (int)GlobalVariableGet(TkMovedName()) : 0;
}
void TkSaveSettings()
{
GlobalVariableSet("TKTA_S_RISK", g_riskPct);
GlobalVariableSet("TKTA_S_RISKIN", InpRiskPercent);
GlobalVariableSet("TKTA_S_FOLD", g_fold ? 1 : 0);
GlobalVariableSet("TKTA_S_DET", g_det ? 1 : 0);
GlobalVariableSet("TKTA_S_FLV", g_fontLv);
GlobalVariableSet("TKTA_S_RR", g_rrSel);
GlobalVariableSet("TKTA_S_TP", g_showTP ? 1 : 0);
GlobalVariableSet("TKTA_S_DIR", g_dir);
GlobalVariableSet("TKTA_S_GUIDE", g_guideOff ? 1 : 0);
if(g_px >= 0 && g_py >= 0)
{
GlobalVariableSet("TKTA_S_X", g_px);
GlobalVariableSet("TKTA_S_Y", g_py);
}
GlobalVariablesFlush();
}
void TkSaveMoved() { GlobalVariableSet(TkMovedName(), g_mv); }
//+------------------------------------------------------------------+
// Number formatting |
//+------------------------------------------------------------------+
// thousands separators
string TkCommas(const string s)
{
int dot = StringFind(s, ".");
string ip = (dot >= 0) ? StringSubstr(s, 0, dot) : s;
string fp = (dot >= 0) ? StringSubstr(s, dot) : "";
string out = "";
int n = StringLen(ip);
for(int i = 0; i < n; i++)
{
if(i > 0 && (n - i) % 3 == 0) out += ",";
out += StringSubstr(ip, i, 1);
}
return out + fp;
}
// decimals of the account currency
int TkMoneyDigits()
{
string c = AccountInfoString(ACCOUNT_CURRENCY);
if(c == "JPY" || c == "KRW" || c == "HUF" || c == "IDR" || c == "VND" || c == "CLP") return 0;
return 2;
}
// Money text. A JPY account in Japanese shows the amount with the yen sign, otherwise "100.00 USD". Minus sign is U+2212
// Money text. A JPY account in Japanese shows the amount with the yen sign, otherwise "100.00 USD". Minus sign is U+2212
string TkMoney(const double v, const bool plusSign)
{
int d = TkMoneyDigits();
string a = DoubleToString(MathAbs(v), d);
string sign = "";
if(StringToDouble(a) != 0.0) sign = (v < 0) ? "−" : (plusSign ? "+" : "");
string cur = AccountInfoString(ACCOUNT_CURRENCY);
if(g_ja && cur == "JPY") return sign + TkCommas(a) + "円";
return sign + TkCommas(a) + " " + cur;
}
// percent text (1.25% for 0.25 steps, otherwise one decimal)
string TkPct(const double p)
{
double q = MathRound(p * 100.0) / 100.0;
int d = (MathAbs(q * 10.0 - MathRound(q * 10.0)) > 1e-6) ? 2 : 1;
return DoubleToString(q, d);
}
// ==== LOT-CORE BEGIN ====
// decimals from the lot step
int TkLotDigits(const double step)
{
if(step <= 0) return 2;
int d = 0;
double s = step;
while(d < 8 && MathAbs(s - MathRound(s)) > 1e-9) { s *= 10.0; d++; }
return d;
}
//+------------------------------------------------------------------+
// Pip size and pip value |
// Almost the same code in the MT4 version. |
//+------------------------------------------------------------------+
string TkUpper(string s) { StringToUpper(s); return s; }
bool TkIsLetter(const ushort c) { return (c >= 'A' && c <= 'Z'); }
// is it gold? (2026-10-22): anything containing XAU is gold. GOLD counts only in a symbol-like form (GOLD, GOLDm, GOLD.a, GOLDUSD, #GOLD ...).
// Stocks and ETFs such as BARRICKGOLD and GOLDMAN are not treated as gold
// Gold (2026-10-22): any name with XAU. GOLD only in symbol form (GOLD, GOLDm, GOLD.a, GOLDUSD, #GOLD);
// stock/ETF names such as BARRICKGOLD or GOLDMAN are not gold
bool TkIsGold(const string sym)
{
string u = TkUpper(sym);
if(StringFind(u, "XAU") >= 0) return true;
int p = StringFind(u, "GOLD");
if(p < 0) return false;
if(p > 0 && TkIsLetter(StringGetCharacter(u, p - 1))) return false; // letter before → another name
string rest = StringSubstr(u, p + 4);
int n = 0;
while(n < StringLen(rest) && TkIsLetter(StringGetCharacter(rest, n))) n++;
if(n <= 2) return true; // none or a short tag
string w = StringSubstr(rest, 0, n);
if(w == "MINI" || w == "MICRO") return true;
if(n != 3) return false;
// followed by a 3-letter currency (e.g. GOLDUSD)
return (w == "USD" || w == "EUR" || w == "JPY" || w == "GBP" || w == "AUD" || w == "CHF" || w == "CAD"
|| w == "NZD" || w == "SGD" || w == "HKD" || w == "CNH");
}
// Pip size
// - if overridePip > 0, use it (overridden in the settings)
// - Gold (XAU in the name, or GOLD in a symbol-like form; TkIsGold) has no fixed definition (brokers and descriptions differ: 0.01, 0.1, ...).
// Use the goldPip setting and show "1 pip = 0.1 (gold setting)" on the panel.
// Gold has no single pip definition; the goldPip setting is used and shown on the panel.
// - If the price has 3 or 5 digits (Digits) -> 10 x the minimum step (Point) (USDJPY 0.001 -> 0.01, EURUSD 0.00001 -> 0.0001).
// Digits 3 or 5 → pip = 10 x Point.
// - Otherwise (2, 4, 1 or 0 digits: old-style brokers, stock indices ...) -> Point as it is.
// Otherwise (2/4-digit brokers, indices, ...) → pip = Point.
double TkPipSizeOf(const string sym, const double overridePip, const double goldPip)
{
if(overridePip > 0) return overridePip;
if(TkIsGold(sym)) return goldPip;
int dg = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
double pt = SymbolInfoDouble(sym, SYMBOL_POINT);
if(dg == 3 || dg == 5) return pt * 10.0;
return pt;
}
// Find the conversion symbol
// Put into rate how much 1 unit of from (the profit currency) is worth in to (the account currency). mid = the current price of that symbol (middle of bid and ask).
// rate = how many 'to' for 1 'from'. Direct pair (from+to, e.g. USDJPY for USD→JPY) or inverse (to+from).
// Symbols with a broker suffix or prefix (USDJPY.m / USDJPYmicro ...) are also used if the name contains the pair and the currency settings match.
// Broker suffixes/prefixes are accepted when the name contains the pair and the currencies match.
// returns: 1 = same currency, 0 = found, -1 = none, -2 = exists but is not shown in Market Watch or has no price
// rate of 1 'from' from one symbol
bool TkRateFromName(const string nm, const string from, double &rate, double &mid)
{
double b = SymbolInfoDouble(nm, SYMBOL_BID), a = SymbolInfoDouble(nm, SYMBOL_ASK);
if(!(bool)SymbolInfoInteger(nm, SYMBOL_SELECT) || b <= 0 || a <= 0) return false;
mid = (a + b) / 2.0;
rate = (SymbolInfoString(nm, SYMBOL_CURRENCY_BASE) == from) ? mid : 1.0 / mid;
return true;
}
// Search all symbols for a direct from/to symbol (including ones with a broker suffix or prefix)
// Scan all symbols for a direct pair (broker suffixes/prefixes accepted)
// returns: 0 = usable, -2 = exists but not in Market Watch or no price (hidden), -1 = none
int TkScanDirect(const string from, const string to, string &pair)
{
string d1 = from + to, d2 = to + from, hidden = "";
int n = SymbolsTotal(false);
for(int i = 0; i < n; i++)
{
string nm = SymbolName(i, false);
string up = TkUpper(nm);
string cb = SymbolInfoString(nm, SYMBOL_CURRENCY_BASE);
string cp = SymbolInfoString(nm, SYMBOL_CURRENCY_PROFIT);
bool direct = (StringFind(up, d1) >= 0 && cb == from && cp == to);
bool inverse = (StringFind(up, d2) >= 0 && cb == to && cp == from);
if(!direct && !inverse) continue;
double r = 0, m = 0;
if(!TkRateFromName(nm, from, r, m)) { if(hidden == "") hidden = nm; continue; }
pair = nm;
return 0;
}
pair = (hidden != "") ? hidden : d1;
return (hidden != "") ? -2 : -1;
}
// Remember the search result (searching all symbols is done only when the currency changes, or 10 seconds after a failure)
// Cache the scan result (full scan only when the currencies change, or 10 s after a failure)
string g_cfKey = "", g_cfN1 = "", g_cfN2 = "", g_cfFail = "";
int g_cfCode = -1;
uint g_cfScanAt = 0;
void TkConvScan(const string from, const string to)
{
g_cfKey = from + "/" + to; g_cfScanAt = GetTickCount();
g_cfN1 = ""; g_cfN2 = ""; g_cfFail = "";
string p1 = "";
int c1 = TkScanDirect(from, to, p1);
if(c1 == 0) { g_cfN1 = p1; g_cfCode = 0; return; }
// If there is no direct symbol, go via USD (e.g. HKD -> USDHKD -> USDJPY -> JPY)
if(from != "USD" && to != "USD")
{
string pa = "", pb = "";
int ca = TkScanDirect(from, "USD", pa);
int cb = TkScanDirect("USD", to, pb);
if(ca == 0 && cb == 0) { g_cfN1 = pa; g_cfN2 = pb; g_cfCode = 0; return; }
if(c1 == -1 && (ca == -2 || cb == -2)) { g_cfFail = (ca == -2) ? pa : pb; g_cfCode = -2; return; }
}
g_cfFail = p1; g_cfCode = c1;
}
// Put into rate how much 1 unit of from (the profit currency) is worth in to (the account currency). mid = the current price of the symbol used.
// rate = how many 'to' for 1 'from'. pair = the symbol used ("A+B" when going via USD).
// returns: 1 = same currency, 0 = found, -1 = none, -2 = not in Market Watch or no price (hidden or no price)
int TkFindConv(const string from, const string to, string &pair, double &rate, double &mid)
{
pair = ""; rate = 0; mid = 0;
if(from == "" || to == "") return -1;
if(from == to) { rate = 1.0; return 1; }
if(from + "/" + to != g_cfKey) TkConvScan(from, to);
for(int attempt = 0; attempt < 2; attempt++)
{
if(g_cfCode == 0)
{
double r1 = 0, m1 = 0, r2 = 0, m2 = 0;
if(g_cfN2 == "")
{
if(TkRateFromName(g_cfN1, from, r1, m1)) { pair = g_cfN1; rate = r1; mid = m1; return 0; }
}
else if(TkRateFromName(g_cfN1, from, r1, m1) && TkRateFromName(g_cfN2, "USD", r2, m2))
{
pair = g_cfN1 + "+" + g_cfN2; rate = r1 * r2; mid = m1; return 0;
}
}
if(attempt == 0 && GetTickCount() - g_cfScanAt >= 10000) TkConvScan(from, to); // search again only once every 10 seconds
else break;
}
if(g_cfCode == 0) { pair = g_cfN1; return -2; } // the cached symbol lost its price
pair = g_cfFail;
return g_cfCode;
}
// Pip value per lot, in this order:
// 1) MT5: the terminal's own profit calculation (OrderCalcProfit; it only calculates and places no order). Same as the real profit.
// MT5: the terminal's own profit calculation (OrderCalcProfit, calculation only) = the same math as real P/L.
// 2) When 1) cannot be used (MT4, or when trying another currency): contract size per lot x pip x conversion price.
// Compare with the broker's TICK_VALUE and show no number if they differ by more than 5% (code=3). However, when TICK_VALUE is
// "left in the profit currency (not converted)", accept it as a broker display quirk (common with indices).
// Otherwise: contract size x pip x conversion rate, cross-checked with TICK_VALUE (>5% → no number, code 3),
// except the common pattern where TICK_VALUE is still in the profit currency (not converted).
// 3) Symbols such as futures, whose profit is not price difference x contract size, use TICK_VALUE only.
// Futures and similar: TICK_VALUE only.
// Overriding the contract size per lot (contractOverride > 0) works for FX only. It calculates with overridden size x pip x conversion price.
// Contract-size override is for FX only.
// result code: 0 = OK, 1 = no conversion symbol (no pair), 2 = not in Market Watch (hidden), 3 = mismatch, 4 = no symbol data (no data), 5 = symbol cannot be overridden (override not possible)
double g_cvRate = 0, g_cvMid = 0, g_cvContract = 0, g_cvTick = 0, g_cvCross = 0;
string g_cvPair = "";
int g_cvState = -1;
bool g_cvLinear = true; // profit = price move x contract size
bool g_cvFx = true; // FX (stop in pips)
bool g_cvIndex = false; // CFD index
int g_cvSource = 0; // 1 = terminal calculation, 2 = contract size x conversion rate, 3 = TICK_VALUE only
// Get the value of 1 pip per 1 lot from the terminal's own profit calculation (MT5 only; a calculation-only function)
// Pip value per lot from the terminal's own profit calculation (MT5 only; calculation only)
bool TkTerminalVpp(const string sym, const double pip, double &vpp)
{
vpp = 0;
double p = SymbolInfoDouble(sym, SYMBOL_BID);
if(p <= 0 || pip <= 0) return false;
double d = pip * 10.0; // compute for 10 pips, divide by 10
// The terminal's answer is rounded to 1 cent, so calculate for 100 lots to reduce the error (1 lot if that fails)
// The terminal rounds to cents, so compute for 100 lots to shrink the rounding error (fall back to 1 lot)
double vols[] = {100.0, 1.0};
for(int k = 0; k < 2; k++)
{
double up = 0, dn = 0;
if(!OrderCalcProfit(ORDER_TYPE_BUY, sym, vols[k], p, p + d, up) || up <= 0) continue;
if(p - d > 0 && OrderCalcProfit(ORDER_TYPE_BUY, sym, vols[k], p, p - d, dn) && -dn > up) up = -dn; // use the larger (loss side)
vpp = up / 10.0 / vols[k];
return true;
}
return false;
}
// acc = account currency (normally ACCOUNT_CURRENCY; the check tool passes JPY / USD / EUR)
// acc = account currency (normally ACCOUNT_CURRENCY; the check script passes JPY / USD / EUR)
int TkCalcVpp(const string sym, const double pip, const double contractOverride, const string acc, double &vpp)
{
vpp = 0; g_cvTick = 0; g_cvCross = 0; g_cvSource = 0;
string prof = SymbolInfoString(sym, SYMBOL_CURRENCY_PROFIT);
double brokerContract = SymbolInfoDouble(sym, SYMBOL_TRADE_CONTRACT_SIZE);
g_cvContract = (contractOverride > 0) ? contractOverride : brokerContract;
double ts = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
double tv = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
g_cvTick = (ts > 0 && tv > 0 && pip > 0) ? tv * pip / ts : 0;
// TICK_VALUE is in the real account currency
bool realAcc = (acc == AccountInfoString(ACCOUNT_CURRENCY));
if(!realAcc) g_cvTick = 0;
long mode = SymbolInfoInteger(sym, SYMBOL_TRADE_CALC_MODE);
g_cvFx = (mode == SYMBOL_CALC_MODE_FOREX || mode == SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE);
g_cvIndex = (mode == SYMBOL_CALC_MODE_CFDINDEX);
g_cvLinear = (g_cvFx || mode == SYMBOL_CALC_MODE_CFD || g_cvIndex || mode == SYMBOL_CALC_MODE_CFDLEVERAGE);
g_cvState = TkFindConv(prof, acc, g_cvPair, g_cvRate, g_cvMid);
if(pip <= 0) return 4;
if(contractOverride > 0 && !g_cvFx) return 5;
// terminal calculation
if(contractOverride <= 0 && realAcc && TkTerminalVpp(sym, pip, vpp)) { g_cvSource = 1; return 0; }
// futures etc.
if(!g_cvLinear)
{
if(g_cvTick <= 0) return 4;
vpp = g_cvTick; g_cvSource = 3;
return 0;
}
// contract x pip x rate
if(g_cvState == -1) return 1;
if(g_cvState == -2) return 2;
g_cvCross = brokerContract * pip * g_cvRate;
if(g_cvTick > 0 && g_cvCross > 0 && MathAbs(g_cvTick - g_cvCross) > 0.05 * g_cvCross)
{
// accepted broker patterns:
// a) TICK_VALUE is left in the profit currency (not converted)
// b) CFD index, and TICK_VALUE is the value per 1 unit of contract size (in the profit currency)
// Same idea as the MT5 CFD index profit formula = (price difference) x contract size x TICK_VALUE / TICK_SIZE
double unconv = brokerContract * pip;
bool a = (g_cvRate != 1.0 && unconv > 0 && MathAbs(g_cvTick - unconv) <= 0.05 * unconv);
bool b = (g_cvIndex && brokerContract > 0 && MathAbs(g_cvTick * brokerContract / g_cvRate - unconv) <= 0.05 * unconv);
if(!a && !b) return 3;
}
vpp = (contractOverride > 0) ? contractOverride * pip * g_cvRate : g_cvCross;
g_cvSource = 2;
return (vpp > 0) ? 0 : 4;
}
// lots (rounded down to the volume step)
// returns 0 when below the minimum lot
bool g_lotCapped = false; // capped at the broker maximum
double TkLotsFor(const string sym, const double riskMoney, const double stopPips, const double vpp)
{
g_lotCapped = false;
double mn = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
double mx = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
double st = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
if(riskMoney <= 0 || stopPips <= 0 || vpp <= 0 || st <= 0) return 0;
double raw = riskMoney / (stopPips * vpp);
// +1e-7 guards against floating point error
double lots = MathFloor(raw / st + 1e-7) * st;
if(mx > 0 && lots > mx) { lots = MathFloor(mx / st + 1e-7) * st; g_lotCapped = true; }
lots = NormalizeDouble(lots, TkLotDigits(st));
if(lots < mn - 1e-9) return 0;
return lots;
}
// ==== LOT-CORE END ====
//+------------------------------------------------------------------+
// Lot size |
//+------------------------------------------------------------------+
double g_lotRisk = 0, g_lotLots = 0, g_lotLoss = 0, g_lotVpp = 0, g_lotMin = 0, g_lotBase = 0;
bool g_lotOk = false, g_lotBelowMin = false;
double TkGoldPip() { return (InpGoldPip == TK_GOLDPIP_001) ? 0.01 : (InpGoldPip == TK_GOLDPIP_1) ? 1.0 : 0.1; }
double TkContractOverride()
{
if(InpContract == TK_CONTRACT_AUTO) return 0;
if(InpContract == TK_CONTRACT_CUSTOM) return (InpContractCustom > 0) ? InpContractCustom : 0;
return (double)InpContract;
}
int g_vppCode = 4; // result code
double g_stopPips = 0; // stop distance taken from the lines (pips)
int g_side = 0; // 1 long, -1 short, 0 same price
int g_lastSide = 0; // previous side (long/short), to detect a flip (2026-10-22)
uint g_flipAt = 0; // when the side flipped (0 = never)
double g_defDist = 0; // first stop line distance (price)
bool g_defAtrOk = false; // g_defDist was decided by ATR (also true in fixed-pips mode)
// Distance text: pips for FX, price for others
string TkDistText(const double priceDiff)
{
// huge pip counts (e.g. BTCUSD) → price
bool hugePips = (g_pip > 0 && priceDiff / g_pip > 10000.0);
if(g_pip <= 0 || (g_cvFx && !hugePips)) return DoubleToString((g_pip > 0) ? priceDiff / g_pip : 0, 1) + T("pips", " pips");
return T("値段で ", "price ") + DoubleToString(priceDiff, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
}
// Convert the distance between the two lines to pips. It is divided after aligning to the minimum price step, so 20 pips comes out as exactly 20.0
// Distance between two lines in pips. Snapped to the point first, so 20 pips becomes exactly 20.0
double TkPipsBetween(const double a, const double b)
{
double pt = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(g_pip <= 0 || pt <= 0) return 0;
double pts = MathRound(MathAbs(a - b) / pt);
return NormalizeDouble(pts * pt / g_pip, 4);
}
// lots (the calculation is the same as 1.00; only the stop distance now comes from the lines)
// Lots (same math as 1.00; only the stop distance now comes from the lines)
void TkCalcLot()
{
g_lotOk = false; g_lotBelowMin = false; g_lotLots = 0; g_lotLoss = 0;
g_lotBase = (InpRiskBase == TK_BASE_EQUITY) ? AccountInfoDouble(ACCOUNT_EQUITY) : AccountInfoDouble(ACCOUNT_BALANCE);
g_lotRisk = (InpRiskMode == TK_RISK_PERCENT) ? g_lotBase * g_riskPct / 100.0 : InpRiskMoney;
g_lotMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
if(g_vppCode != 0 || g_lotRisk <= 0 || g_stopPips <= 0) return;
double lots = TkLotsFor(_Symbol, g_lotRisk, g_stopPips, g_lotVpp);
if(lots <= 0) { g_lotBelowMin = true; g_lotLoss = g_lotMin * g_stopPips * g_lotVpp; return; }
g_lotLots = lots;
g_lotLoss = lots * g_stopPips * g_lotVpp;
g_lotOk = true;
}
// Message when the value cannot be converted
string TkVppMessage()
{
if(g_vppCode == 1 || g_vppCode == 2)
return T("換算できません(" + g_cvPair + " を気配値表示に出してください)", "Cannot convert (please show " + g_cvPair + " in Market Watch)");
if(g_vppCode == 3)
return T("計算できません(業者の値 TICK_VALUE と通貨量の計算が合わないため)", "Cannot calculate (the broker's TICK_VALUE does not match the contract size)");
if(g_vppCode == 5)
return T("この銘柄は1ロットの通貨量を上書きできません(自動にしてください)", "Units per lot cannot be overridden for this symbol (use Auto)");
return T("計算できません(銘柄の情報がまだありません)", "Cannot calculate (no symbol data yet)");
}
string TkLotText(const double lots)
{
return DoubleToString(lots, TkLotDigits(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP)));
}
// ==== TIME-CORE BEGIN ====
//+------------------------------------------------------------------+
// Period boundary time |
// Same code as the MT4 version. |
//+------------------------------------------------------------------+
// Idea:
// 1) current server time -> UTC -> time in the boundary's region (NY or Japan).
// 2) shift so that the boundary time becomes 0:00 (NY 17:00 is +7 hours / Japan's H o'clock is -H hours).
// NY 17:00 is "the start of the next trading day", so with +7 Sunday 17:00 becomes "Monday 0:00".
// Shift so the boundary becomes 00:00 (NY 17:00: +7h, so Sunday 17:00 NY = Monday of the trading week).
// 3) in the shifted time, round down to the start of the day, the start of Monday, or the 1st of the month.
// 4) shift back the other way, UTC -> server time.
// NY daylight saving time follows the US rule (from the 2nd Sunday of March 2:00 to the 1st Sunday of November 2:00), applied automatically.
// US DST: from the 2nd Sunday of March 02:00 to the 1st Sunday of November 02:00 (local).
// With a NY 17:00 boundary, it is 7:00 in winter and 6:00 in summer in Japan time.
// Note: the difference server time - UTC uses the value of "now". If the broker switches daylight saving in the middle of a month,
// the start of a one-month period can be off by one hour. The current server offset is used for past boundaries.
// Day of week, 0 = Sunday (1970-01-01 was a Thursday)
int TkDow(const datetime t)
{
return (int)((((long)t / 86400) + 4) % 7);
}
datetime TkDate(const int y, const int m, const int d)
{
MqlDateTime s;
ZeroMemory(s);
s.year = y; s.mon = m; s.day = d;
return StructToTime(s);
}
int TkYear(const datetime t)
{
MqlDateTime s;
TimeToStruct(t, s);
return s.year;
}
// 0:00 of the n-th Sunday of month m
datetime TkNthSunday(const int y, const int m, const int n)
{
datetime f = TkDate(y, m, 1);
int add = (7 - TkDow(f)) % 7;
return (datetime)((long)f + (long)(add + 7 * (n - 1)) * 86400);
}
// Is the US on DST at this UTC instant?
// start = 2nd Sunday of March 2:00 EST = 7:00 UTC, end = 1st Sunday of November 2:00 EDT = 6:00 UTC
bool TkUsDstUtc(const datetime utc)
{
int y = TkYear(utc);
long a = (long)TkNthSunday(y, 3, 2) + 7 * 3600;
long b = (long)TkNthSunday(y, 11, 1) + 6 * 3600;
return ((long)utc >= a && (long)utc < b);
}
// Is it daylight saving time in NY local time? (the boundary is 17:00, so the ambiguous hour around 2:00 does not matter)
// Is NY local time on DST? (The ambiguous hour around 02:00 never matters for a 17:00 boundary.)
bool TkUsDstNyLocal(const datetime ny)
{
int y = TkYear(ny);
long a = (long)TkNthSunday(y, 3, 2) + 2 * 3600;
long b = (long)TkNthSunday(y, 11, 1) + 1 * 3600;
return ((long)ny >= a && (long)ny < b);
}
// Floor to day, Monday, or 1st of month
// period: 0 = day, 1 = week, 2 = month
datetime TkFloorPeriod(const datetime s, const int period)
{
long v = (long)s;
long day = v - (v % 86400);
if(period == 1)
{
int back = (TkDow((datetime)day) + 6) % 7; // days back to Monday
return (datetime)(day - (long)back * 86400);
}
if(period == 2)
{
MqlDateTime d;
TimeToStruct(s, d);
return TkDate(d.year, d.mon, 1);
}
return (datetime)day;
}
// period start (server time)
// serverNow: current server time / serverOffset: server time - UTC (seconds)
// boundary: 0 = NY 17:00, 1 = server 0:00, 2 = jstHour o'clock in Japan time / period: 0 = day, 1 = week, 2 = month
datetime TkPeriodStartServer(const datetime serverNow, const long serverOffset, const int boundary, const int jstHour, const int period)
{
if(boundary == 1) return TkFloorPeriod(serverNow, period); // server 00:00
long utc = (long)serverNow - serverOffset;
long utcStart;
if(boundary == 0)
{
long ny = utc + (TkUsDstUtc((datetime)utc) ? -4 : -5) * 3600; // NY local
long shifted = ny + 7 * 3600; // 17:00 → 0:00
long ps = (long)TkFloorPeriod((datetime)shifted, period);
long nyStart = ps - 7 * 3600;
utcStart = nyStart + (TkUsDstNyLocal((datetime)nyStart) ? 4 : 5) * 3600;
}
else
{
int h = MathMax(0, MathMin(23, jstHour));
long jst = utc + 9 * 3600; // Japan time (no daylight saving)
long shifted = jst - (long)h * 3600; // H:00 → 0:00
long ps = (long)TkFloorPeriod((datetime)shifted, period);
utcStart = ps + (long)h * 3600 - 9 * 3600;
}
return (datetime)(utcStart + serverOffset);
}
// Round the server offset to 15 minutes
long TkRoundOffset(const long diff)
{
return (long)MathRound((double)diff / 900.0) * 900;
}
// ==== TIME-CORE END ====
//+------------------------------------------------------------------+
//| Period record from deals (MT5 counts per deal) |
//+------------------------------------------------------------------+
// - Count only buy/sell deals (deposits, withdrawals, bonuses and so on are excluded).
// Only BUY/SELL deals (balance, credit, bonus... are excluded).
// - What is counted is "trades closed within the period". They are grouped by position ID, so a partial close
// split into several deals counts as one (no double counting). Positions not closed yet are not included.
// Counted: trades closed inside the period, grouped by position ID (partial closes count once). Open positions are excluded.
// - Profit = profit + swap + commission + other fees of the closing deals in the period
// + commission and fees of the opening deal (only when the first close of that position is inside the period; never counted twice in two periods).
// Same idea as the MT4 version (which includes the whole commission of a closed order). Opening deals are searched back 90 days.
// Net = closing deals in the period (profit + swap + commission + fee) + the entry deal's commission/fee
// (only when the position's first close is inside the period). Same idea as the MT4 version. Entry deals are searched 90 days back.
// - Losing streak = looking at positions closed within the period, newest first, how many losses in a row.
// Losing streak = consecutive losing positions, newest first, within the period.
// dictionary: position ID -> row index (open addressing hash)
long g_hKeys[];
int g_hVals[];
int g_hMask = 0;
void TkHashInit(const int n)
{
int size = 64;
while(size < n * 2) size *= 2;
ArrayResize(g_hKeys, size);
ArrayResize(g_hVals, size);
ArrayInitialize(g_hVals, -1);
g_hMask = size - 1;
}
// returns the row, or inserts newIndex and returns -1
int TkHashGetOrAdd(const long key, const int newIndex)
{
int h = (int)((ulong)(key * 2654435761) & (ulong)g_hMask);
while(g_hVals[h] >= 0)
{
if(g_hKeys[h] == key) return g_hVals[h];
h = (h + 1) & g_hMask;
}
g_hKeys[h] = key;
g_hVals[h] = newIndex;
return -1;
}
int g_pDealsSeen = -1; // deal count at the last scan
datetime g_pStartSeen = 0; // period start at the last scan
void TkCalcPeriod(const datetime serverNow)
{
datetime start = TkPeriodStartServer(serverNow, g_srvOffset, (int)InpBoundary, InpJstHour, (int)InpPeriod);
if(!HistorySelect((datetime)((long)start - 90 * 86400), (datetime)((long)serverNow + 86400))) { g_pOk = false; return; }
int total = HistoryDealsTotal();
// If nothing new has started (same count and same start), do not read again (closed deals do not change)
if(g_pOk && total == g_pDealsSeen && start == g_pStartSeen) return;
g_pDealsSeen = total; g_pStartSeen = start;
g_pOk = false; g_pNet = 0; g_pTrades = 0; g_pStreak = 0;
g_pStart = start;
long ids[];
double outNet[], entryCost[];
datetime firstOut[], last[];
bool closedIn[];
int m = 0;
TkHashInit(total);
for(int i = 0; i < total; i++)
{
ulong tk = HistoryDealGetTicket(i);
if(tk == 0) continue;
long type = HistoryDealGetInteger(tk, DEAL_TYPE);
if(type != DEAL_TYPE_BUY && type != DEAL_TYPE_SELL) continue;
if(InpScope == TK_SCOPE_SYMBOL && HistoryDealGetString(tk, DEAL_SYMBOL) != _Symbol) continue;
datetime t = (datetime)HistoryDealGetInteger(tk, DEAL_TIME);
long pid = HistoryDealGetInteger(tk, DEAL_POSITION_ID);
long entry = HistoryDealGetInteger(tk, DEAL_ENTRY);
int k = TkHashGetOrAdd(pid, m);
if(k < 0)
{
k = m; m++;
if(ArraySize(ids) < m)
{
int cap = MathMax(64, m * 2);
ArrayResize(ids, cap); ArrayResize(outNet, cap); ArrayResize(entryCost, cap);
ArrayResize(firstOut, cap); ArrayResize(last, cap); ArrayResize(closedIn, cap);
}
ids[k] = pid; outNet[k] = 0; entryCost[k] = 0; firstOut[k] = 0; last[k] = 0; closedIn[k] = false;
}
double cost = HistoryDealGetDouble(tk, DEAL_COMMISSION) + HistoryDealGetDouble(tk, DEAL_FEE);
if(entry == DEAL_ENTRY_IN)
{
entryCost[k] += cost;
continue;
}
if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT && entry != DEAL_ENTRY_OUT_BY) continue;
if(firstOut[k] == 0 || t < firstOut[k]) firstOut[k] = t;
if(t < start) continue;
outNet[k] += HistoryDealGetDouble(tk, DEAL_PROFIT) + HistoryDealGetDouble(tk, DEAL_SWAP) + cost;
closedIn[k] = true;
if(t > last[k]) last[k] = t;
}
// Pick out only what closed within the period, sort by close time (newest first) and count the losing streak
// Take the positions closed in the period, sort by last close (newest first), count the losing streak
double order[][2]; // [close time, row]
int c = 0;
ArrayResize(order, m);
for(int j = 0; j < m; j++)
{
if(!closedIn[j]) continue;
double net = outNet[j] + ((firstOut[j] >= start) ? entryCost[j] : 0.0);
outNet[j] = net;
g_pNet += net;
g_pTrades++;
order[c][0] = (double)last[j];
order[c][1] = (double)j;
c++;
}
ArrayResize(order, c);
if(c > 0) ArraySort(order); // oldest first
for(int r = c - 1; r >= 0; r--)
{
if(outNet[(int)order[r][1]] < 0) g_pStreak++; else break;
}
g_pOk = true;
}
//+------------------------------------------------------------------+
// Colors |
//+------------------------------------------------------------------+
int TkLum(const color c)
{
int r = (int)(c & 0xFF), g = (int)((c >> 8) & 0xFF), b = (int)((c >> 16) & 0xFF);
return (r * 299 + g * 587 + b * 114) / 1000;
}
// shift a color lighter (+) or darker (−)
color TkShift(const color c, const int d)
{
int r = (int)(c & 0xFF), g = (int)((c >> 8) & 0xFF), b = (int)((c >> 16) & 0xFF);
r = MathMax(0, MathMin(255, r + d)); g = MathMax(0, MathMin(255, g + d)); b = MathMax(0, MathMin(255, b + d));
return (color)(r | (g << 8) | (b << 16));
}
void TkSetColors()
{
color bg = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
g_lastBg = bg;
// entry text: darker on light charts
g_cEntryText = (TkLum(bg) < 128) ? C'175,175,175' : C'85,85,85';
// When a panel color is chosen, choose the text color pair from the panel brightness (2026-10-22: dark text on a black panel was unreadable)
// If a panel color is chosen, pick the text palette by the panel's brightness
bool custom = (InpPanelColor != clrNONE && !(InpBackground == TK_BG_NONE));
bool dark = custom ? (TkLum(InpPanelColor) < 128) : (TkLum(bg) < 128);
if(dark)
{
g_cText = C'210,210,210'; g_cMuted = C'140,140,140'; g_cHead = C'240,240,240';
g_cWarn = C'230,190,60'; g_cAlert = C'255,110,110';
}
else
{
g_cText = C'50,50,50'; g_cMuted = C'115,115,115'; g_cHead = C'15,15,15';
g_cWarn = C'170,105,0'; g_cAlert = C'200,30,30';
}
// The background is a subdued color slightly shifted from the chart background. When see-through, it is drawn as a bitmap (ARGB)
// Background: the chart background shifted a little; see-through modes draw it as an ARGB bitmap
g_cPanel = custom ? InpPanelColor : TkShift(bg, dark ? 18 : -12);
g_cBorder = TkShift(g_cPanel, dark ? 28 : -34);
g_cBtn = TkShift(g_cPanel, dark ? 22 : -20);
// For see-through or no background, move the text to the darkest black or white so it stays readable over candles behind it (2026-10-22)
// See-through or no background: push the text to near black / near white so it stays readable over candles
if(InpBackground != TK_BG_SOLID) { g_cText = g_cHead; g_cMuted = dark ? C'190,190,190' : C'70,70,70'; }
if(InpTextColor != clrNONE) g_cText = InpTextColor;
// If the text and panel brightness are close, it is unreadable (e.g. both black). Then the text goes back to the automatic color
// Text too close to the panel in brightness (e.g. both black) → fall back to the automatic text color
if(MathAbs(TkLum(g_cText) - TkLum((InpBackground == TK_BG_NONE) ? bg : g_cPanel)) < 80) g_cText = dark ? C'210,210,210' : C'50,50,50';
g_font = g_ja ? "Meiryo UI" : "Segoe UI";
}
//+------------------------------------------------------------------+
// Panel layout |
//| Inside a row, place text segments (seg) from the left. right=true aligns right |
//| Each row holds text pieces left to right; right=true pieces go right |
//+------------------------------------------------------------------+
#define BTN_NONE -1
#define BTN_GRIP 1 // grip (drag to move)
#define BTN_FOLD 2
#define BTN_UNFOLD 3
#define BTN_RMINUS 4
#define BTN_RPLUS 5
#define BTN_FMINUS 6
#define BTN_FPLUS 7
#define BTN_TP 8
#define BTN_DETAIL 9
#define BTN_GUIDE 10
#define BTN_RESET 11
#define BTN_RR1 12 // 1:1
#define BTN_RR15 13 // 1:1.5
#define BTN_RR2 14 // 1:2
#define BTN_RR3 15 // 1:3
#define BTN_BUY 16 // long
#define BTN_SELL 17 // short
#define BTN_TPOFF 18 // hide the target line
#define BTN_OFF 99 // disabled button (at the end of its range)
color g_sBg[]; // button background
string g_sT[]; int g_sF[]; color g_sC[]; int g_sB[]; bool g_sR[]; bool g_sBox[]; int g_sRow[];
int g_sX[]; int g_sY[]; int g_sW[]; int g_sH[];
int g_rGap[];
int g_nS = 0, g_nR = 0, g_objN = 0;
int g_panX = 0, g_panY = 0, g_panW = 0, g_panH = 0;
void TkClear() { g_nS = 0; g_nR = 0; }
void TkRow(const int gap = 0)
{
g_nR++;
ArrayResize(g_rGap, g_nR);
g_rGap[g_nR - 1] = gap;
}
void TkSeg(const string s, const int fs, const color c, const int btn = BTN_NONE, const bool right = false, const bool box = false)
{
if(g_nR == 0) TkRow();
g_nS++;
ArrayResize(g_sT, g_nS); ArrayResize(g_sF, g_nS); ArrayResize(g_sC, g_nS); ArrayResize(g_sB, g_nS);
ArrayResize(g_sR, g_nS); ArrayResize(g_sBox, g_nS); ArrayResize(g_sRow, g_nS); ArrayResize(g_sBg, g_nS);
int i = g_nS - 1;
g_sT[i] = s; g_sF[i] = MathMax(6, fs); g_sC[i] = c; g_sB[i] = btn; g_sR[i] = right; g_sBox[i] = box; g_sRow[i] = g_nR - 1; g_sBg[i] = g_cBtn;
}
void TkBtn(const string s, const int fs, const int btn, const bool right = false) { TkSeg(s, fs, g_cText, btn, right, true); }
// button with its own colors
void TkBtnC(const string s, const int fs, const int btn, const bool right, const color txt, const color bg)
{
TkSeg(s, fs, txt, btn, right, true);
g_sBg[g_nS - 1] = bg;
}
// disabled button (faint text, background same as the panel)
void TkBtnOff(const string s, const int fs, const bool right) { TkBtnC(s, fs, BTN_OFF, right, TkShift(g_cMuted, (TkLum(g_cPanel) < 128) ? -45 : 70), g_cPanel); }
void TkLabel(const string nm, const int x, const int y, const string s, const int fs, const color c)
{
if(ObjectFind(0, nm) < 0)
{
ObjectCreate(0, nm, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, nm, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, nm, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, nm, OBJPROP_BACK, false);
ObjectSetInteger(0, nm, OBJPROP_ZORDER, 10);
}
ObjectSetInteger(0, nm, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, nm, OBJPROP_YDISTANCE, y);
ObjectSetString(0, nm, OBJPROP_TEXT, s);
ObjectSetString(0, nm, OBJPROP_FONT, g_font);
ObjectSetInteger(0, nm, OBJPROP_FONTSIZE, fs);
ObjectSetInteger(0, nm, OBJPROP_COLOR, c);
}
void TkBox(const string nm, const int x, const int y, const int w, const int h, const color bg, const color border)
{
if(ObjectFind(0, nm) < 0)
{
ObjectCreate(0, nm, OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, nm, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, nm, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, nm, OBJPROP_BACK, false);
ObjectSetInteger(0, nm, OBJPROP_ZORDER, 10);
}
ObjectSetInteger(0, nm, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, nm, OBJPROP_YDISTANCE, y);
ObjectSetInteger(0, nm, OBJPROP_XSIZE, w);
ObjectSetInteger(0, nm, OBJPROP_YSIZE, h);
ObjectSetInteger(0, nm, OBJPROP_BGCOLOR, bg);
ObjectSetInteger(0, nm, OBJPROP_COLOR, border);
}
// see-through background drawn as a bitmap
string g_bgRes = "::TKTA_BG";
int g_bgW = 0, g_bgH = 0;
color g_bgC = clrNONE;
void TkBmpBox(const string nm, const int x, const int y, const int w, const int h, const uchar alpha)
{
if(w <= 0 || h <= 0) return;
if(ObjectFind(0, nm) < 0)
{
ObjectCreate(0, nm, OBJ_BITMAP_LABEL, 0, 0, 0);
ObjectSetInteger(0, nm, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, nm, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, nm, OBJPROP_BACK, false);
ObjectSetInteger(0, nm, OBJPROP_ZORDER, 10);
g_bgW = 0; // rebuild the bitmap
}
if(w != g_bgW || h != g_bgH || g_cPanel != g_bgC)
{
uint px[];
ArrayResize(px, w * h);
uint fill = ColorToARGB(g_cPanel, alpha);
uint edge = ColorToARGB(g_cBorder, (uchar)MathMin(255, alpha + 60));
for(int j = 0; j < h; j++)
for(int i = 0; i < w; i++)
px[j * w + i] = (i == 0 || j == 0 || i == w - 1 || j == h - 1) ? edge : fill;
if(ResourceCreate(g_bgRes, px, w, h, 0, 0, w, COLOR_FORMAT_ARGB_NORMALIZE))
{ g_bgW = w; g_bgH = h; g_bgC = g_cPanel; }
ObjectSetString(0, nm, OBJPROP_BMPFILE, g_bgRes);
}
ObjectSetInteger(0, nm, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, nm, OBJPROP_YDISTANCE, y);
}
void TkRender()
{
int bpx = 6, bpy = 2, gapX = 6, pad = 8, rowSp = 3;
ArrayResize(g_sX, g_nS); ArrayResize(g_sY, g_nS); ArrayResize(g_sW, g_nS); ArrayResize(g_sH, g_nS);
int rowH[], leftW[], rightW[];
ArrayResize(rowH, g_nR); ArrayResize(leftW, g_nR); ArrayResize(rightW, g_nR);
ArrayInitialize(rowH, 0); ArrayInitialize(leftW, 0); ArrayInitialize(rightW, 0);
// Measure the text size (negative value = units of 0.1 point; the same scale as the OBJ_LABEL font size)
// Measure text (negative size = tenths of a point, same scale as the label font size)
for(int i = 0; i < g_nS; i++)
{
TextSetFont(g_font, -g_sF[i] * 10);
uint w = 0, h = 0;
TextGetSize(g_sT[i], w, h);
int wi = (int)w, hi = (int)h;
// If the same text was drawn before, use the larger of the actual drawn width and height (XSIZE/YSIZE of OBJ_LABEL are read-only actual sizes).
// With a display scaling factor and so on, the measured width can be smaller than the real one (2026-10-22: it overflowed on the right in the test MT5)
// If the same text was drawn before, use the larger of the measured and the actual drawn size
string mn = TK_PANEL + "L" + IntegerToString(i);
if(ObjectFind(0, mn) >= 0 && ObjectGetString(0, mn, OBJPROP_TEXT) == g_sT[i] && ObjectGetInteger(0, mn, OBJPROP_FONTSIZE) == g_sF[i])
{
int aw = (int)ObjectGetInteger(0, mn, OBJPROP_XSIZE), ah = (int)ObjectGetInteger(0, mn, OBJPROP_YSIZE);
if(aw > wi) wi = aw;
if(ah > hi) hi = ah;
}
// a little slack until the actual size is known
wi = (int)MathCeil(wi * 1.06) + 1;
if(g_sBox[i]) { wi += 2 * bpx; hi += 2 * bpy; }
g_sW[i] = wi; g_sH[i] = hi;
int r = g_sRow[i];
if(hi > rowH[r]) rowH[r] = hi;
if(g_sR[i]) rightW[r] += ((rightW[r] > 0) ? gapX : 0) + wi;
else leftW[r] += ((leftW[r] > 0) ? gapX : 0) + wi;
}
int maxW = 0, sumH = 0;
for(int r = 0; r < g_nR; r++)
{
int rw = leftW[r] + ((rightW[r] > 0) ? ((leftW[r] > 0) ? 18 : 0) + rightW[r] : 0);
if(rw > maxW) maxW = rw;
sumH += rowH[r] + g_rGap[r];
}
int pw = maxW + pad * 2;
int ph = sumH + pad * 2 + rowSp * MathMax(0, g_nR - 1);
int cw = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
int ch = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0);
int x0, y0;
if(g_px < 0 || g_py < 0)
{
// not moved yet: from the chosen corner
bool right = (InpCorner == CORNER_RIGHT_UPPER || InpCorner == CORNER_RIGHT_LOWER);
bool bottom = (InpCorner == CORNER_LEFT_LOWER || InpCorner == CORNER_RIGHT_LOWER);
x0 = right ? cw - InpOffsetX - pw : InpOffsetX;
y0 = bottom ? ch - InpOffsetY - ph : InpOffsetY;
}
else { x0 = g_px; y0 = g_py; }
// keep inside the chart
// right edge ≤ width − 10 px; move up if too low
x0 = MathMax(0, MathMin(x0, cw - 10 - pw));
y0 = MathMax(0, MathMin(y0, ch - 4 - ph));
// When the chart is smaller than the panel (minimized and so on), do not overwrite the remembered position (go back to the original place at the original size) 2026-10-22
// When the chart is smaller than the panel (e.g. minimized), keep the stored position
if(g_px >= 0 && g_py >= 0 && cw >= pw + 10 && ch >= ph + 4) { g_px = x0; g_py = y0; }
g_panX = x0; g_panY = y0; g_panW = pw; g_panH = ph;
// positions
int y = y0 + pad, cur = -1, lx = 0, rx = 0;
for(int i = 0; i < g_nS; i++)
{
int r = g_sRow[i];
while(cur < r)
{
if(cur >= 0) y += rowH[cur] + rowSp;
cur++;
y += g_rGap[cur];
lx = x0 + pad;
rx = x0 + pw - pad - rightW[cur];
}
if(g_sR[i]) { g_sX[i] = rx; rx += g_sW[i] + gapX; }
else { g_sX[i] = lx; lx += g_sW[i] + gapX; }
g_sY[i] = y + (rowH[r] - g_sH[i]); // align to the bottom of the row
}
// draw
string bgName = TK_PANEL + "ANEL";
if(InpBackground == TK_BG_NONE) ObjectDelete(0, bgName);
else
{
// When there is no background, recreate everything (so the background does not end up above the text)
if(ObjectFind(0, bgName) < 0) { ObjectsDeleteAll(0, TK_PANEL); g_objN = 0; }
if(InpBackground == TK_BG_SOLID) TkBox(bgName, x0, y0, pw, ph, g_cPanel, g_cBorder);
else TkBmpBox(bgName, x0, y0, pw, ph, (uchar)((InpBackground == TK_BG_LIGHT) ? 190 : 150)); // Slightly see-through: about 75% / More see-through: about 60%
}
for(int i = 0; i < g_nS; i++)
{
string ln = TK_PANEL + "L" + IntegerToString(i);
string bn = TK_PANEL + "B" + IntegerToString(i);
if(g_sBox[i])
{
if(ObjectFind(0, bn) < 0) ObjectDelete(0, ln); // text after its box
TkBox(bn, g_sX[i], g_sY[i], g_sW[i], g_sH[i], g_sBg[i], g_cBorder);
TkLabel(ln, g_sX[i] + bpx, g_sY[i] + bpy, g_sT[i], g_sF[i], g_sC[i]);
}
else
{
ObjectDelete(0, bn);
TkLabel(ln, g_sX[i], g_sY[i], g_sT[i], g_sF[i], g_sC[i]);
}
}
for(int i = g_nS; i < g_objN; i++)
{
ObjectDelete(0, TK_PANEL + "L" + IntegerToString(i));
ObjectDelete(0, TK_PANEL + "B" + IntegerToString(i));
}
g_objN = g_nS;
}
bool TkInPanel(const int x, const int y)
{
return (x >= g_panX && x < g_panX + g_panW && y >= g_panY && y < g_panY + g_panH);
}
bool TkInGrip(const int x, const int y)
{
for(int i = 0; i < g_nS && i < ArraySize(g_sX); i++)
if(g_sB[i] == BTN_GRIP && x >= g_sX[i] - 4 && x < g_sX[i] + g_sW[i] + 4 && y >= g_sY[i] - 4 && y < g_sY[i] + g_sH[i] + 4) return true;
return false;
}
//+------------------------------------------------------------------+
//| Chart lines (display only; no orders are placed) |
//+------------------------------------------------------------------+
void TkMakeLine(const string name, const double price, const color c, const int style, const int width, const string desc)
{
if(ObjectFind(0, name) >= 0) return; // keep the old position (e.g. after changing the timeframe)
ObjectCreate(0, name, OBJ_HLINE, 0, 0, price);
ObjectSetInteger(0, name, OBJPROP_COLOR, c);
ObjectSetInteger(0, name, OBJPROP_STYLE, style);
ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, true);
ObjectSetInteger(0, name, OBJPROP_SELECTED, true); // draggable right away
ObjectSetInteger(0, name, OBJPROP_BACK, false);
ObjectSetInteger(0, name, OBJPROP_ZORDER, 5);
ObjectSetString(0, name, OBJPROP_TOOLTIP, desc);
}
void TkSetLine(const string name, const double price)
{
double p = NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
double pt = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(MathAbs(ObjectGetDouble(0, name, OBJPROP_PRICE) - p) > pt * 0.1) ObjectSetDouble(0, name, OBJPROP_PRICE, p);
}
// keep the lines always "selected" (one press is enough to drag)
void TkKeepSelected(const string name)
{
if(ObjectFind(0, name) >= 0 && !(bool)ObjectGetInteger(0, name, OBJPROP_SELECTED)) ObjectSetInteger(0, name, OBJPROP_SELECTED, true);
}
void TkRRDelete()
{
ObjectDelete(0, TK_RR_ENTRY); ObjectDelete(0, TK_RR_ENTRY + "_T");
ObjectDelete(0, TK_RR_SL); ObjectDelete(0, TK_RR_SL + "_T");
ObjectDelete(0, TK_RR_TP); ObjectDelete(0, TK_RR_TP + "_T");
ObjectDelete(0, TK_RR_SYM);
}
// remove lines that belong to another symbol
void TkRRCheckSymbol()
{
if(ObjectFind(0, TK_RR_SYM) >= 0 && ObjectGetString(0, TK_RR_SYM, OBJPROP_TEXT) != _Symbol)
{
TkRRDelete(); // symbol changed
g_mv = 0; TkSaveMoved();
}
}
// ATR (14 bars, up to the previous bar)
double TkAtr(const int n)
{
if(Bars(_Symbol, _Period) < n + 2) return 0;
double s = 0;
for(int i = 1; i <= n; i++)
{
double h = iHigh(_Symbol, _Period, i), l = iLow(_Symbol, _Period, i), pc = iClose(_Symbol, _Period, i + 1);
if(h <= 0 || l <= 0 || pc <= 0) return 0;
s += MathMax(h - l, MathMax(MathAbs(h - pc), MathAbs(l - pc)));
}
return s / n;
}
// first stop line distance in price
double TkDefaultDist()
{
double pt = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
double d = ((InpStopPips > 0) ? InpStopPips : 20.0) * g_pip;
double a = (InpInitStop == TK_INIT_PIPS) ? 0 : TkAtr(14);
g_defAtrOk = (InpInitStop == TK_INIT_PIPS) || (a > 0);
if(a > 0)
{
d = a * MathMax(0.1, InpAtrMult);
if(InpInitStop == TK_INIT_AUTO)
{
// round to a round number (pips for FX, price for others. e.g. 23.7 -> 25, 2.37 -> 2.5)
// Round to a tidy number (pips for FX, price otherwise; e.g. 23.7 → 25, 2.37 → 2.5)
bool inPips = (g_cvFx && g_pip > 0);
double v = inPips ? d / g_pip : d;
if(v > 0)
{
double half = MathPow(10.0, MathFloor(MathLog10(v))) / 2.0;
v = MathMax(half, MathRound(v / half) * half);
}
if(inPips) v = MathMax(10.0, v); // FX: at least 10 pips
d = inPips ? v * g_pip : v;
}
}
if(pt > 0) d = MathMax(pt, MathRound(d / pt) * pt);
return d;
}
double TkNowPrice()
{
double p = SymbolInfoDouble(_Symbol, SYMBOL_BID);
if(p <= 0) p = iClose(_Symbol, _Period, 0);
return p;
}
// create lines, keep undragged lines in place
void TkLinesEnsure()
{
TkRRCheckSymbol();
if(ObjectFind(0, TK_RR_SYM) < 0)
{
ObjectCreate(0, TK_RR_SYM, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, TK_RR_SYM, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
ObjectSetInteger(0, TK_RR_SYM, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, TK_RR_SYM, OBJPROP_HIDDEN, true);
}
ObjectSetString(0, TK_RR_SYM, OBJPROP_TEXT, _Symbol);
int mv0 = g_mv;
double now = TkNowPrice();
if(now <= 0) return;
if(ObjectFind(0, TK_RR_ENTRY) < 0)
{
TkMakeLine(TK_RR_ENTRY, now, g_cEntry, STYLE_DASH, 1, T("入る値(目安の線。ドラッグで動かせます)", "Entry (reference line, drag to move)"));
g_mv &= ~1;
}
if((g_mv & 1) == 0) TkSetLine(TK_RR_ENTRY, now); // follows the price until dragged
double e = ObjectGetDouble(0, TK_RR_ENTRY, OBJPROP_PRICE);
if(ObjectFind(0, TK_RR_SL) < 0)
{
TkMakeLine(TK_RR_SL, e - g_dir * g_defDist, g_cSL, STYLE_SOLID, 2, T("損切り(目安の線。ドラッグで動かせます)", "Stop loss (reference line, drag to move)"));
g_mv &= ~2;
}
if((g_mv & 2) == 0) TkSetLine(TK_RR_SL, e - g_dir * g_defDist); // on the chosen side
double s = ObjectGetDouble(0, TK_RR_SL, OBJPROP_PRICE);
if(g_showTP)
{
// the first target line is at 1:2 (on the opposite side of the stop)
double d = (MathAbs(e - s) > 0) ? (e - s) : g_defDist;
double tp = e + ((g_rrSel > 0) ? g_rrSel : 2.0) * d; // follows at the chosen ratio
if(tp <= 0) tp = _Point; // guard for a very low price on a short
if(ObjectFind(0, TK_RR_TP) < 0)
{
TkMakeLine(TK_RR_TP, tp, g_cTP, STYLE_SOLID, 1, T("利確(目安の線。ドラッグで動かせます)", "Target (reference line, drag to move)"));
g_mv &= ~4;
}
if((g_mv & 4) == 0) TkSetLine(TK_RR_TP, tp);
}
else
{
ObjectDelete(0, TK_RR_TP); ObjectDelete(0, TK_RR_TP + "_T");
g_mv &= ~4;
}
TkKeepSelected(TK_RR_ENTRY); TkKeepSelected(TK_RR_SL); TkKeepSelected(TK_RR_TP);
if(g_mv != mv0) TkSaveMoved();
}
// text next to a line (placed at the position of the leftmost visible bar)
// above=true puts it above the line, false below (so texts of nearby lines do not overlap)
void TkLineText(const string line, const string s, const color c, const int fs, const bool above)
{
string nm = line + "_T";
if(ObjectFind(0, line) < 0) { ObjectDelete(0, nm); return; }
double p = ObjectGetDouble(0, line, OBJPROP_PRICE);
// place at the time 40px from the left of the screen (so the left end of the text is inside the visible range)
// Place at the time found 40 px from the left edge, so the start of the text is visible
datetime t = 0;
int sub = 0; double pp = 0;
if(!ChartXYToTimePrice(0, 40, 20, sub, t, pp) || t <= 0)
{
// fallback: the 3rd visible bar from the left
long first = ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR, 0) - 3;
int bars = Bars(_Symbol, _Period);
if(first >= bars) first = bars - 1;
if(first < 0) first = 0;
t = iTime(_Symbol, _Period, (int)first);
}
if(ObjectFind(0, nm) < 0)
{
ObjectCreate(0, nm, OBJ_TEXT, 0, t, p);
ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, nm, OBJPROP_HIDDEN, true);
}
ObjectSetInteger(0, nm, OBJPROP_ANCHOR, above ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER);
ObjectMove(0, nm, 0, t, p);
ObjectSetString(0, nm, OBJPROP_TEXT, s);
ObjectSetString(0, nm, OBJPROP_FONT, g_font);
ObjectSetInteger(0, nm, OBJPROP_FONTSIZE, fs);
ObjectSetInteger(0, nm, OBJPROP_COLOR, c);
}
//+------------------------------------------------------------------+
// Rebuild the panel |
//+------------------------------------------------------------------+
// distance to stop-out (the same formula in the check tool)
double TkStopOutRoom(const double equity, const double margin, const double level, const bool percentMode)
{
if(percentMode) return equity - margin * level / 100.0;
return (equity - margin) - level;
}
string TkTwo(const int v) { return (v < 10 ? "0" : "") + IntegerToString(v); }
// required margin for the calculated lot (calculation only; no order is placed). 0 if unknown (made a function on 2026-10-22)
// Margin for the calculated lot (calculation only). 0 when unknown
double TkLotMargin()
{
if(!g_lotOk || g_side == 0) return 0;
double m1 = 0;
ENUM_ORDER_TYPE ot = (g_side > 0) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
double px = (g_side > 0) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
// OrderCalcMargin is a calculation-only function (it sends no order)
if(!OrderCalcMargin(ot, _Symbol, g_lotLots, px, m1)) return 0;
return (m1 > 0) ? m1 : 0;
}
// does the required margin for the calculated lot exceed the free margin? (the lot is not reduced) 2026-10-22
// Does the margin for the lot exceed free margin? (the lot is not reduced)
bool TkMarginShort(const double m1) { return (m1 > 0 && m1 > AccountInfoDouble(ACCOUNT_MARGIN_FREE)); }
// Contents of "More"
void TkBuildDetails(const datetime serverNow, const bool forceHistory, const int fsN, const int fsS)
{
int dg = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
// Margin
if(InpShowMargin)
{
TkRow(6); TkSeg(T("証拠金と維持率", "Margin"), fsS, g_cHead);
double eq = AccountInfoDouble(ACCOUNT_EQUITY);
double mg = AccountInfoDouble(ACCOUNT_MARGIN);
double free = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
double so = AccountInfoDouble(ACCOUNT_MARGIN_SO_SO);
double call = AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL);
bool pct = (AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE) == ACCOUNT_STOPOUT_MODE_PERCENT);
string soTxt = pct ? DoubleToString(so, 0) + "%" : TkMoney(so, false);
if(mg <= 0)
{
TkRow(); TkSeg(T("今はポジションがありません(維持率 -)", "No open positions (margin level -)"), fsN, g_cText);
TkRow(); TkSeg(T("ロスカットの水準 ", "Stop-out level ") + soTxt, fsS, g_cMuted);
}
else
{
double lvl = eq / mg * 100.0;
// percent method: equity - required margin x level% / amount method: free margin - stop-out amount
double dist = TkStopOutRoom(eq, mg, so, pct);
color c = g_cText;
if(pct && call > 0 && lvl <= call) c = g_cAlert;
else if(pct && call > 0 && lvl <= call * 1.5) c = g_cWarn;
TkRow();
if(dist > 0) TkSeg(T("あと約 " + TkMoney(dist, false) + " の損失でロスカットの水準", "About " + TkMoney(dist, false) + " more loss reaches the stop-out level"), fsN, c);
else TkSeg(T("ロスカットの水準に達しています", "At or below the stop-out level"), fsN, g_cAlert);
TkRow(); TkSeg(T("維持率 ", "Margin level ") + DoubleToString(lvl, 1) + "%" + T("(ロスカット ", " (stop-out ") + soTxt + ")", fsS, c);
}
TkRow(); TkSeg(T("使っている証拠金 ", "Margin used ") + TkMoney(mg, false) + T(" / 余力 ", " / free ") + TkMoney(free, false), fsS, g_cMuted);
double m1 = TkLotMargin();
if(m1 > 0)
{
bool shortM = TkMarginShort(m1); // more than free margin → red
TkRow(); TkSeg(T("このロットの必要証拠金 約 ", "Margin for this lot size about ") + TkMoney(m1, false), fsS, shortM ? g_cAlert : g_cMuted);
}
}
// Period record
if(InpShowPeriod)
{
if(forceHistory || TimeLocal() - g_lastHist >= 5) { TkCalcPeriod(serverNow); g_lastHist = TimeLocal(); }
string pName = (InpPeriod == TK_PERIOD_DAY) ? T("1日", "day") : (InpPeriod == TK_PERIOD_WEEK) ? T("1週間", "week") : T("1か月", "month");
string bName = (InpBoundary == TK_BOUND_NY1700) ? T("NY 17:00 区切り", "NY 17:00 close")
: (InpBoundary == TK_BOUND_SERVER) ? T("サーバー 0:00 区切り", "server 00:00")
: T("日本時間 " + IntegerToString(InpJstHour) + ":00 区切り", "Japan time " + IntegerToString(InpJstHour) + ":00");
string head = (InpScope == TK_SCOPE_SYMBOL) ? T("この銘柄の確定した損益", "Closed P/L of this symbol") : T("口座全体の確定した損益", "Closed P/L of the whole account");
TkRow(6); TkSeg(head + T("(" + pName + "・" + bName + ")", " (" + pName + ", " + bName + ")"), fsS, g_cHead);
if(!g_pOk) { TkRow(); TkSeg(T("履歴を読めませんでした", "Could not read the history"), fsN, g_cWarn); }
else
{
// MT4: the history we could read starts in the middle of the period -> the closed profit shows too small (2026-10-22)
// MT4: the loaded history starts inside the period, so the closed P/L may be too small
if(g_pHistShort)
{
TkRow(); TkSeg(T("口座履歴のタブで「全履歴」を選んでください", "Choose \"All History\" in the Account History tab"), fsN, g_cAlert);
TkRow(); TkSeg(T("(読めた履歴が期間の途中からで、損益が少なく出ているかもしれません)", "(the loaded history starts after the period start, so the P/L may be incomplete)"), fsS, g_cWarn);
}
color cNet = g_cText;
if(InpLimitLoss > 0 && g_pNet < 0)
{
if(-g_pNet > InpLimitLoss) cNet = g_cAlert;
else if(-g_pNet >= InpLimitLoss * 0.8) cNet = g_cWarn;
}
string lim = (InpLimitLoss > 0) ? T(" / 上限 ", " / limit ") + TkMoney(-InpLimitLoss, false) : "";
TkRow(); TkSeg(TkMoney(g_pNet, true) + lim, fsN, cNet);
color cCnt = g_cText;
if(InpLimitTrades > 0)
{
if(g_pTrades > InpLimitTrades) cCnt = g_cAlert;
else if(g_pTrades >= InpLimitTrades * 0.8) cCnt = g_cWarn;
}
string limT = (InpLimitTrades > 0) ? T(" / 上限 ", " / limit ") + IntegerToString(InpLimitTrades) : "";
TkRow(); TkSeg(T("取引 ", "Trades ") + IntegerToString(g_pTrades) + T(" 回", "") + limT + T(" / 今の連敗 ", " / losing streak ") + IntegerToString(g_pStreak), fsS, cCnt);
// show the start in PC local time
MqlDateTime ls;
TimeToStruct((datetime)((long)g_pStart - g_srvOffset + ((long)TimeLocal() - (long)TimeGMT())), ls);
TkRow(); TkSeg(T("始まり ", "Since ") + IntegerToString(ls.mon) + "/" + IntegerToString(ls.day) + " " + TkTwo(ls.hour) + ":" + TkTwo(ls.min)
+ T("(PC の時刻)・手数料とスワップ込み", " (PC time), incl. commission and swap"), fsS, g_cMuted);
}
}
// Spread and bar countdown
if(InpShowSpread)
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
string sp = (g_pip > 0 && ask > 0 && bid > 0) ? DoubleToString((ask - bid) / g_pip, 1) + " pips" : "-";
datetime barStart = iTime(_Symbol, _Period, 0);
long next;
if(_Period == PERIOD_MN1)
{
MqlDateTime d;
TimeToStruct(barStart, d);
next = (long)TkDate(d.mon == 12 ? d.year + 1 : d.year, d.mon == 12 ? 1 : d.mon + 1, 1);
}
else next = (long)barStart + PeriodSeconds(_Period);
long rem = next - (long)serverNow;
string remTxt = "-";
if(barStart > 0 && rem >= 0 && rem <= PeriodSeconds(_Period) + 86400)
{
long hh = rem / 3600, mm = (rem % 3600) / 60, ss = rem % 60;
remTxt = (hh > 0 ? IntegerToString(hh) + ":" + TkTwo((int)mm) : IntegerToString(mm)) + ":" + TkTwo((int)ss);
}
TkRow(6); TkSeg(T("スプレッド ", "Spread ") + sp + T(" / 足の確定まで ", " / bar closes in ") + remTxt, fsS, g_cText);
// estimate of the extra amount the current spread can add (display only; the lot calculation is unchanged) 2026-10-22
// Rough extra loss from the current spread (display only; the lot calculation is unchanged)
if(g_lotOk && g_vppCode == 0 && g_lotVpp > 0 && g_pip > 0 && ask > bid && bid > 0)
{
double spCost = g_lotLots * ((ask - bid) / g_pip) * g_lotVpp;
TkRow(); TkSeg(T("このロットでスプレッドの分 約 ", "Spread on this lot size about ") + TkMoney(spCost, false), fsS, g_cMuted); // keep the notes text only once, below (2026-10-22 review: duplicate)
}
}
// What the numbers are based on
TkRow(6); TkSeg(T("計算の元", "Calculation basis"), fsS, g_cHead);
bool over = (TkContractOverride() > 0);
TkRow(); TkSeg(T("1ロット = ", "1 lot = ") + TkCommas(DoubleToString(g_cvContract, 0)) + " " + SymbolInfoString(_Symbol, SYMBOL_CURRENCY_BASE)
+ (over ? T("(上書き中)", " (override)") : T("(業者の設定)", " (broker setting)")), fsS, over ? g_cWarn : g_cMuted);
if(g_cvState == 1)
{ TkRow(); TkSeg(T("換算: なし(口座と同じ通貨 ", "Conversion: none (account currency ") + AccountInfoString(ACCOUNT_CURRENCY) + ")", fsS, g_cMuted); }
else if(g_cvState == 0)
{
TkRow();
if(StringFind(g_cvPair, "+") >= 0) TkSeg(T("換算: ", "Conversion: ") + g_cvPair + T("(ドル経由)", " (via USD)"), fsS, g_cMuted);
else TkSeg(T("換算: ", "Conversion: ") + g_cvPair + " " + DoubleToString(g_cvMid, (int)SymbolInfoInteger(g_cvPair, SYMBOL_DIGITS)), fsS, g_cMuted);
}
string pipNote = (InpPipSize > 0) ? T("(上書き中)", " (override)") : TkIsGold(_Symbol) ? T("(ゴールドの設定)", " (gold setting)") : "";
TkRow(); TkSeg("1 pip = " + DoubleToString(g_pip, TkLotDigits(g_pip)) + pipNote, fsS, (InpPipSize > 0) ? g_cWarn : g_cMuted);
if(g_vppCode == 0 && g_lotVpp > 0)
{
string src = (g_cvSource == 1) ? T("端末の損益の計算", "terminal profit calc") : (g_cvSource == 2) ? T("通貨量 × 換算", "contract x rate") : "TICK_VALUE";
TkRow();
if(g_cvFx) TkSeg(T("1ロットで 1pip = ", "1 pip per 1 lot = ") + TkMoney(g_lotVpp, false) + " (" + src + ")", fsS, g_cMuted);
else TkSeg(T("1ロットで値段 1 = ", "Price move 1 per 1 lot = ") + TkMoney(g_lotVpp / g_pip, false) + " (" + src + ")", fsS, g_cMuted);
}
// Notes
TkRow(6); TkSeg(T("スリッページ・手数料・窓開けは含みません", "Excludes slippage, fees and price gaps"), fsS, g_cMuted);
// added
TkRow(); TkSeg(T("スプレッドの分、実際の損失は少し大きくなることがあります", "The spread can make the actual loss a little larger"), fsS, g_cMuted);
TkRow(); TkSeg(T("注文の前に、端末の注文画面で確かめてください", "Check your terminal's order window before trading"), fsS, g_cMuted);
TkRow(4); TkBtn(T("線をはじめの位置に戻す", "Reset the lines"), fsS, BTN_RESET);
}
void TkRefresh(const bool forceHistory)
{
datetime serverNow = TimeTradeServer();
// If the chart background color has changed, decide the colors again and rebuild all panel parts
// (a background color change does not send CHARTEVENT_CHART_CHANGE, or it can arrive too early, so check on every refresh)
// If the chart background changed, redo the colors and rebuild every panel object (checked on every refresh)
if((color)ChartGetInteger(0, CHART_COLOR_BACKGROUND) != g_lastBg)
{
TkSetColors();
ObjectsDeleteAll(0, TK_PANEL); g_objN = 0;
ObjectDelete(0, TK_RR_ENTRY + "_T"); ObjectDelete(0, TK_RR_SL + "_T"); ObjectDelete(0, TK_RR_TP + "_T");
}
g_srvOffset = TkRoundOffset((long)serverNow - (long)TimeGMT());
g_pip = TkPipSizeOf(_Symbol, InpPipSize, TkGoldPip());
g_vppCode = TkCalcVpp(_Symbol, g_pip, TkContractOverride(), AccountInfoString(ACCOUNT_CURRENCY), g_lotVpp);
// While ATR is not available yet (right after switching symbols and so on) use a provisional width. While the red line is not moved, take it again when available
// (an unmoved red line follows g_defDist, so the lines are placed again too)
// Without ATR data a fallback width is used; until the stop line is dragged, retry and re-place the line
if(g_defDist <= 0 || (!g_defAtrOk && (g_mv & 2) == 0)) g_defDist = TkDefaultDist();
// Lines
TkLinesEnsure();
bool haveLines = (ObjectFind(0, TK_RR_ENTRY) >= 0 && ObjectFind(0, TK_RR_SL) >= 0);
double e = haveLines ? ObjectGetDouble(0, TK_RR_ENTRY, OBJPROP_PRICE) : 0;
double s = haveLines ? ObjectGetDouble(0, TK_RR_SL, OBJPROP_PRICE) : 0;
g_stopPips = haveLines ? TkPipsBetween(e, s) : 0;
g_side = 0;
if(haveLines && g_stopPips > 0) g_side = (s < e) ? 1 : -1;
// If long/short flips (e.g. the price crosses the stop line), make it stand out on the panel for 8 seconds (2026-10-22)
// If the side flips (e.g. the price crossed the stop line), highlight it on the panel for 8 s
if(g_side != 0 && g_lastSide != 0 && g_side != g_lastSide) { g_flipAt = GetTickCount(); if(g_flipAt == 0) g_flipAt = 1; }
if(g_side != 0) g_lastSide = g_side;
if(g_side != 0 && g_side != g_dir) { g_dir = g_side; TkSaveSettings(); } // buttons follow the lines
bool flipped = (g_flipAt != 0 && GetTickCount() - g_flipAt < 8000);
TkCalcLot();
//--- State: show the number or the reason
bool slMoved = ((g_mv & 2) != 0);
if(slMoved && !g_guideOff) { g_guideOff = true; TkSaveSettings(); }
string why = ""; // red reason
string hint = ""; // hint (muted)
if(g_vppCode != 0) why = TkVppMessage();
else if(!haveLines) why = T("値段がまだありません", "No price yet");
else if(g_side == 0) why = T("損切りの線が入る値と同じ所にあります", "The stop line is at the entry price");
else if(!slMoved) hint = T("赤の線を損切りの所へ動かすと計算します", "Move the red line to your stop to calculate");
else if(g_lotRisk <= 0) why = T("リスクの額が 0 です(設定を確かめてください)", "The risk amount is 0 (check the settings)");
else if(g_lotBelowMin) why = T("最小ロット " + TkLotText(g_lotMin) + " でも 約 " + TkMoney(-g_lotLoss, false) + " になり、決めた額を超えます",
// sign made the same as in the Japanese text (2026-10-22)
"Even the minimum lot " + TkLotText(g_lotMin) + " is about " + TkMoney(-g_lotLoss, false) + " at the stop, more than your risk amount");
else if(!g_lotOk) why = T("計算できません(入力を確かめてください)", "Cannot calculate (check the inputs)");
bool show = (why == "" && hint == "" && g_lotOk);
// Text sizes
// 5 levels: small … large
int lvAdd[] = {-2, -1, 0, 2, 4};
int base = MathMax(6, InpFontSize + lvAdd[MathMax(0, MathMin(4, g_fontLv))]);
int fsS = MathMax(7, base - 1), fsN = base, fsL = base + 3, fsXL = base * 2 + 4;
string lotStr = show ? TkLotText(g_lotLots) : "--";
string baseName = (InpRiskBase == TK_BASE_EQUITY) ? T("有効証拠金", "equity") : T("残高", "balance");
// side word (red right after a flip)
string sideWord = (g_side > 0) ? T("買い", "BUY") : (g_side < 0) ? T("売り", "SELL") : "";
color sideCol = flipped ? g_cAlert : g_cHead;
// enough free margin? (the lot is not reduced)
bool marginShort = show && TkMarginShort(TkLotMargin());
TkClear();
if(g_fold)
{
// folded: one thin band with the lot size
TkRow();
TkSeg("::", fsN, g_cMuted, BTN_GRIP);
TkSeg(lotStr + T(" ロット", " lots"), fsL, show ? g_cHead : g_cMuted, BTN_GRIP);
if(sideWord != "") TkSeg(sideWord, fsN, sideCol, BTN_GRIP);
if(marginShort) TkSeg(T("証拠金不足", "Low margin"), fsN, g_cAlert, BTN_GRIP);
TkBtn("▼", fsS, BTN_UNFOLD, true);
}
else
{
// row 1 (small)
TkRow();
TkSeg("::", fsN, g_cMuted, BTN_GRIP);
TkSeg(_Symbol + T(" ロット計算", " Lot size"), fsS, g_cMuted, BTN_GRIP);
// faint and inactive at the ends
if(g_fontLv > 0) TkBtn("A−", fsS, BTN_FMINUS, true); else TkBtnOff("A−", fsS, true);
if(g_fontLv < 4) TkBtn("A+", fsS, BTN_FPLUS, true); else TkBtnOff("A+", fsS, true);
TkBtn("▲", fsS, BTN_FOLD, true);
// guide (click once to dismiss; remembered)
if(!g_guideOff)
{
TkRow(2);
TkSeg(T("① 灰の線を入る所へ ② 赤の線を損切りへ", "① Gray line to your entry ② Red line to your stop"), fsS, g_cWarn, BTN_GUIDE);
TkBtn("×", fsS, BTN_GUIDE, true);
}
// row 2 (extra large): lots
TkRow(2);
TkSeg(lotStr + T(" ロット", " lots"), fsXL, show ? g_cHead : g_cMuted);
// long/short is chosen with the buttons (pressing moves the red line to the opposite side of the entry) 2026-10-22
// Choose the side with buttons (the stop line moves to the other side of the entry)
bool isSell = (g_side != 0) ? (g_side < 0) : (g_dir < 0);
color onBg = flipped ? g_cAlert : g_cTP;
if(!isSell) TkBtnC(T("買い", "BUY"), fsN, BTN_BUY, false, C'255,255,255', onBg); else TkBtn(T("買い", "BUY"), fsN, BTN_BUY);
if(isSell) TkBtnC(T("売り", "SELL"), fsN, BTN_SELL, false, C'255,255,255', onBg); else TkBtn(T("売り", "SELL"), fsN, BTN_SELL);
// explanation right after a flip (2026-10-22)
if(flipped)
{
TkRow();
TkSeg((g_side > 0) ? T("売りから買いに変わりました(損切りの線が入る値より下)", "Changed from SELL to BUY (the stop line is below the entry)")
: T("買いから売りに変わりました(損切りの線が入る値より上)", "Changed from BUY to SELL (the stop line is above the entry)"), fsN, g_cAlert);
}
// row 3 (large): amount at the stop
TkRow();
if(show)
{
// when the required margin exceeds the free margin, show it in red in a prominent place (the lot is not reduced) 2026-10-22
// Margin for this lot exceeds free margin: red, prominent (the lot is not reduced)
if(marginShort)
{
TkSeg(T("証拠金が足りません(このロットは注文できない可能性)", "Not enough free margin (this lot may be rejected)"), fsN, g_cAlert);
TkRow();
}
double lossPct = (g_lotBase > 0) ? g_lotLoss / g_lotBase * 100.0 : 0;
string pctTxt = (lossPct > 0 && lossPct < 0.1) ? DoubleToString(lossPct, 2) : DoubleToString(lossPct, 1);
TkSeg(T("損切りで 約 ", "At stop: about ") + TkMoney(-g_lotLoss, false) + T("(" + baseName + "の " + pctTxt + "%)", " (" + pctTxt + "% of " + baseName + ")"), fsL, g_cText);
if(g_lotCapped)
{ TkRow(); TkSeg(T("業者の最大ロット(" + TkLotText(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)) + ")で止めています", "Limited to the broker's maximum lot (" + TkLotText(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)) + ")"), fsS, g_cWarn); }
}
else if(why != "") TkSeg(why, fsN, g_cAlert);
else TkSeg(hint, fsN, g_cMuted);
// row 4: risk
TkRow(2);
if(InpRiskMode == TK_RISK_PERCENT)
{
bool large = (g_riskPct > 3.0 + 1e-9);
TkSeg(T("リスク", "Risk"), fsN, g_cText);
// faint and inactive at the lower and upper limits (2026-10-22)
if(g_riskPct > 0.25 + 1e-9) TkBtn("−", fsN, BTN_RMINUS); else TkBtnOff("−", fsN, false);
TkSeg(TkPct(g_riskPct) + "%", fsN, large ? g_cAlert : g_cHead);
if(g_riskPct < 10.0 - 1e-9) TkBtn("+", fsN, BTN_RPLUS); else TkBtnOff("+", fsN, false);
if(large) TkSeg(T("リスクが3%を超えています", "Risk is above 3%"), fsS, g_cAlert); // neutral wording (2026-10-22)
}
else TkSeg(T("リスク ", "Risk ") + TkMoney(InpRiskMoney, false) + T("(設定の金額)", " (fixed amount in settings)"), fsN, g_cText);
// small note
TkRow(2); TkSeg(T("目安です。このツールは売買しません", "Estimate only. This tool never places trades"), fsS, g_cMuted);
// buttons: target line, more
TkRow(4);
// ratio buttons: the selected ratio changes color. Pressing it again hides the target line
// Ratio buttons: the selected one is colored; pressing it again hides the target line
string rrT[] = {"1:1", "1:1.5", "1:2", "1:3"};
double rrV[] = {1.0, 1.5, 2.0, 3.0};
int rrB[] = {BTN_RR1, BTN_RR15, BTN_RR2, BTN_RR3};
for(int k = 0; k < 4; k++)
{
bool on = (g_showTP && MathAbs(g_rrSel - rrV[k]) < 1e-9);
if(on) TkBtnC(rrT[k], fsS, rrB[k], false, C'255,255,255', g_cTP);
else TkBtn(rrT[k], fsS, rrB[k]);
}
// when the target line is moved by hand, show the current ratio beside it (2026-10-22)
// only when moved on this chart
// plain text (not a button) + [×] to hide
if(g_showTP && g_rrSel <= 0 && (g_mv & 4) != 0 && haveLines && g_side != 0 && ObjectFind(0, TK_RR_TP) >= 0)
{
double t = ObjectGetDouble(0, TK_RR_TP, OBJPROP_PRICE);
bool okSide = (g_side > 0 && t > e) || (g_side < 0 && t < e);
if(okSide && g_stopPips > 0) TkSeg(T("手動 ", "Manual ") + "1:" + DoubleToString(TkPipsBetween(t, e) / g_stopPips, 1), fsN, g_cTP);
else TkSeg(T("手動 1:--(利確が損切り側)", "Manual 1:-- (target on the stop side)"), fsS, g_cAlert);
TkBtn("×", fsS, BTN_TPOFF);
}
TkBtn(g_det ? T("閉じる ▲", "Less ▲") : T("詳しく ▼", "More ▼"), fsS, BTN_DETAIL, true);
if(g_det) TkBuildDetails(serverNow, forceHistory, fsN, fsS);
}
// Text next to the lines
if(haveLines)
{
int dg = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
string sideTxt = (g_side > 0) ? T("(買い)", " (buy)") : (g_side < 0) ? T("(売り)", " (sell)") : "";
// Put text on the side away from the other lines: if the stop is below, entry = above, stop = below, target = above (reversed for a short)
// Text goes on the side away from the other line: long = entry above, stop below, target above (short: reversed)
bool slBelow = (s <= e);
TkLineText(TK_RR_ENTRY, T("入 ", "Entry ") + DoubleToString(e, dg) + sideTxt, g_cEntryText, fsS, slBelow);
string slTxt = T("損切り ", "Stop ") + TkDistText(MathAbs(e - s));
if(show) slTxt += " / " + T("約 ", "about ") + TkMoney(-g_lotLoss, false);
else if(!slMoved && g_side != 0) slTxt += T("(ドラッグで動かせます)", " (drag to move)");
TkLineText(TK_RR_SL, slTxt, g_cSL, fsS, !slBelow);
if(g_showTP && ObjectFind(0, TK_RR_TP) >= 0)
{
double t = ObjectGetDouble(0, TK_RR_TP, OBJPROP_PRICE);
bool ok = (g_side > 0 && t > e) || (g_side < 0 && t < e);
bool tpAbove = (t >= e);
if(!ok) TkLineText(TK_RR_TP, T("利確の線が損切りと同じ側にあります", "The target line is on the stop side"), g_cAlert, fsS, tpAbove);
else
{
double tpPips = TkPipsBetween(t, e);
double ratio = (g_stopPips > 0) ? tpPips / g_stopPips : 0;
string tpTxt = T("利確 ", "Target ") + TkDistText(MathAbs(t - e));
if(show) tpTxt += " / " + T("届いた場合 ", "if reached ") + TkMoney(g_lotLots * tpPips * g_lotVpp, true);
tpTxt += " / 1:" + DoubleToString(ratio, 1);
TkLineText(TK_RR_TP, tpTxt, g_cTP, fsS, tpAbove);
}
}
}
// draw once, read the actual sizes, place again
TkRender();
ChartRedraw(0);
TkRender();
ChartRedraw(0);
}
//+------------------------------------------------------------------+
// Panel buttons |
//+------------------------------------------------------------------+
void TkButton(const int id)
{
bool hist = false;
if(id == BTN_FOLD) g_fold = true;
else if(id == BTN_UNFOLD) g_fold = false;
// [-] cannot go up / [+] cannot go down (when the setting is below 0.25% or above 10%) 2026-10-22
// [-] never raises and [+] never lowers the value (when the setting is below 0.25% or above 10%)
else if(id == BTN_RMINUS) { double nv = MathMax(0.25, MathRound((g_riskPct - 0.25) * 4.0) / 4.0); if(nv >= g_riskPct - 1e-9) return; g_riskPct = nv; }
else if(id == BTN_RPLUS) { double nv = MathMin(10.0, MathRound((g_riskPct + 0.25) * 4.0) / 4.0); if(nv <= g_riskPct + 1e-9) return; g_riskPct = nv; }
else if(id == BTN_FMINUS) { if(g_fontLv <= 0) return; g_fontLv--; }
else if(id == BTN_FPLUS) { if(g_fontLv >= 4) return; g_fontLv++; }
else if(id >= BTN_RR1 && id <= BTN_RR3)
{
double r = (id == BTN_RR1) ? 1.0 : (id == BTN_RR15) ? 1.5 : (id == BTN_RR2) ? 2.0 : 3.0;
if(g_showTP && MathAbs(g_rrSel - r) < 1e-9) { g_showTP = false; g_rrSel = 0; } // pressed again → hide
else { g_showTP = true; g_rrSel = r; g_mv &= ~4; TkSaveMoved(); } // place and follow
}
else if(id == BTN_BUY || id == BTN_SELL)
{
int want = (id == BTN_BUY) ? 1 : -1;
int cur = (g_side != 0) ? g_side : g_dir;
g_dir = want;
if(want != cur && ObjectFind(0, TK_RR_ENTRY) >= 0)
{
// A line moved by hand is moved to the same distance on the opposite side of the entry (lines not moved are placed again automatically)
// Dragged lines are mirrored across the entry (undragged lines are re-placed automatically)
double e = ObjectGetDouble(0, TK_RR_ENTRY, OBJPROP_PRICE);
if((g_mv & 2) != 0 && ObjectFind(0, TK_RR_SL) >= 0)
{
double ns = 2.0 * e - ObjectGetDouble(0, TK_RR_SL, OBJPROP_PRICE);
if(ns > 0) TkSetLine(TK_RR_SL, ns); else { g_mv &= ~2; TkSaveMoved(); }
}
if((g_mv & 4) != 0 && ObjectFind(0, TK_RR_TP) >= 0)
{
double nt = 2.0 * e - ObjectGetDouble(0, TK_RR_TP, OBJPROP_PRICE);
if(nt > 0) TkSetLine(TK_RR_TP, nt); else { g_mv &= ~4; TkSaveMoved(); }
}
g_lastSide = want; g_flipAt = 0; // no flip notice for a button change
}
}
else if(id == BTN_TPOFF) { g_showTP = false; g_rrSel = 0; } // hide the manual target line
else if(id == BTN_DETAIL) { g_det = !g_det; hist = g_det; }
else if(id == BTN_GUIDE) g_guideOff = true;
else if(id == BTN_RESET) { g_mv = 0; TkSaveMoved(); g_defDist = 0; if(g_showTP && g_rrSel <= 0) g_rrSel = 2.0; }
else return;
TkSaveSettings();
TkRefresh(hist);
}
//+------------------------------------------------------------------+
//| Mouse (dragging the grip) |
//+------------------------------------------------------------------+
bool g_mDown = false, g_drag = false, g_scrollOrig = true, g_scrollCur = true;
int g_dragDX = 0, g_dragDY = 0;
uint g_mLast = 0; // last mouse event time
// Stop mouse scrolling so the chart does not move together with the panel (only when it was on originally)
// Over the panel, stop the chart from scrolling with the mouse (only when it was on originally)
void TkScroll(const bool on)
{
if(!g_scrollOrig) return;
if(on == g_scrollCur) return;
ChartSetInteger(0, CHART_MOUSE_SCROLL, on);
g_scrollCur = on;
}
void TkMouse(const int x, const int y, const bool left)
{
g_mLast = GetTickCount();
if(left && !g_mDown && TkInGrip(x, y))
{
g_drag = true;
g_dragDX = x - g_panX; g_dragDY = y - g_panY;
TkScroll(false);
}
else if(left && g_drag)
{
g_px = MathMax(0, x - g_dragDX); g_py = MathMax(0, y - g_dragDY);
TkRender();
ChartRedraw(0);
}
else if(!left && g_drag)
{
g_drag = false;
TkSaveSettings();
}
if(!g_drag) TkScroll(!TkInPanel(x, y));
g_mDown = left;
}
//+------------------------------------------------------------------+
// Init and deinit |
//+------------------------------------------------------------------+
// start working (draw the panel)
void TkStart()
{
g_state = 1;
TkMarkRunning();
IndicatorSetString(INDICATOR_SHORTNAME, "Tiltnote Toolkit");
TkRemoveOldNames();
TkSetColors();
TkLoadSettings();
// With "Chart on foreground" on, the bars and grid are drawn above the panel and it is unreadable (2026-10-22 confirmed by the owner).
// Turn it off only while running and restore it when the tool is removed. The original value is stored per chart in a marker so it survives timeframe changes
// With "Chart on foreground" on, bars and grid are drawn over the panel. Turn it off while running and restore on removal
if(!GlobalVariableCheck(TkForeName())) GlobalVariableSet(TkForeName(), (double)ChartGetInteger(0, CHART_FOREGROUND));
if(ChartGetInteger(0, CHART_FOREGROUND)) ChartSetInteger(0, CHART_FOREGROUND, false);
// receive mouse movement (for dragging the grip). The original value is restored when removed
// Receive mouse moves (for dragging the grip). The original values are restored on removal
if(!GlobalVariableCheck(TkScrollName())) GlobalVariableSet(TkScrollName(), (double)ChartGetInteger(0, CHART_MOUSE_SCROLL));
if(!GlobalVariableCheck(TkMouseName())) GlobalVariableSet(TkMouseName(), (double)ChartGetInteger(0, CHART_EVENT_MOUSE_MOVE));
g_scrollOrig = (GlobalVariableGet(TkScrollName()) > 0.5);
if(g_scrollOrig) ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
g_scrollCur = g_scrollOrig;
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
g_pip = TkPipSizeOf(_Symbol, InpPipSize, TkGoldPip());
TkRefresh(true);
}
int OnInit()
{
if(InpLanguage == TK_LANG_AUTO) g_ja = (TerminalInfoString(TERMINAL_LANGUAGE) == "Japanese");
else g_ja = (InpLanguage == TK_LANG_JA);
MathSrand((int)GetTickCount());
g_instId = (double)MathRand() * 32768.0 + (double)MathRand() + 1.0;
EventSetTimer(1);
// When the timeframe, symbol or settings change, this can run before the cleanup of the old copy.
// So even when the marker is found, do not decide "duplicate" at once; wait and look again with the timer.
// On timeframe/symbol/settings changes this can run before the old copy's OnDeinit,
// so if a marker is found we wait and check again from the timer.
if(TkOtherRunning()) { g_state = 0; g_waitFrom = GetTickCount(); return(INIT_SUCCEEDED); }
TkStart();
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
EventKillTimer();
if(g_state != 1) return; // nothing was drawn
if(GlobalVariableCheck(TkIdName()) && GlobalVariableGet(TkIdName()) == g_instId)
{
GlobalVariableDel(TkInstName());
GlobalVariableDel(TkIdName());
}
ObjectsDeleteAll(0, TK_PANEL);
g_objN = 0;
ResourceFree(g_bgRes); g_bgW = 0; // see-through background bitmap
// restore the mouse settings
if(GlobalVariableCheck(TkScrollName())) ChartSetInteger(0, CHART_MOUSE_SCROLL, GlobalVariableGet(TkScrollName()) > 0.5);
if(GlobalVariableCheck(TkMouseName())) ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, GlobalVariableGet(TkMouseName()) > 0.5);
// If only the timeframe or settings changed, keep the line positions. They are deleted when removed
// Keep the lines on timeframe/parameter change; remove them when the indicator is removed
if(reason != REASON_CHARTCHANGE && reason != REASON_PARAMETERS && reason != REASON_RECOMPILE)
{
TkRRDelete();
GlobalVariableDel(TkMovedName());
GlobalVariableDel(TkScrollName());
GlobalVariableDel(TkMouseName());
// restore the foreground setting when removed
if(GlobalVariableCheck(TkForeName()))
{
if(GlobalVariableGet(TkForeName()) > 0.5) ChartSetInteger(0, CHART_FOREGROUND, true);
GlobalVariableDel(TkForeName());
}
}
else { ObjectDelete(0, TK_RR_ENTRY + "_T"); ObjectDelete(0, TK_RR_SL + "_T"); ObjectDelete(0, TK_RR_TP + "_T"); }
ChartRedraw(0);
}
int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[],
const double &high[], const double &low[], const double &close[], const long &tick_volume[],
const long &volume[], const int &spread[])
{
// Normally updated by the clock (every second). The Strategy Tester may not deliver the clock, so it also updates on price ticks
// Normally refreshed by the 1-second timer; in the Strategy Tester also refresh on ticks
if(MQLInfoInteger(MQL_TESTER) && g_state == 1) TkRefresh(false);
return(rates_total);
}
void OnTimer()
{
if(g_state == 0)
{
// waiting: check again after 2.5 s
if(GetTickCount() - g_waitFrom < 2500) return;
if(TkOtherRunning())
{
// another copy keeps running → real duplicate
g_state = 2;
EventKillTimer();
Alert(T("Tiltnote ツール集は、このチャートに既に付いています(2つ目は動きません)", "Tiltnote Toolkit is already on this chart (this second copy does nothing)"));
return;
}
TkStart(); // the old copy is gone → start normally
return;
}
if(g_state != 1) return;
TkMarkRunning(); // still running
// Releasing the button outside the chart sends no "released" event. If there is no mouse movement for 5 seconds, release the pressed state (2026-10-22)
// Releasing the button outside the chart sends no event: clear the pressed state after 5 s without mouse events
if((g_drag || g_mDown) && GetTickCount() - g_mLast > 5000)
{
if(g_drag) { g_drag = false; TkSaveSettings(); }
g_mDown = false;
TkScroll(true);
}
if(g_drag || g_mDown) return; // not while the mouse button is down
TkRefresh(false);
}
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
if(g_state != 1) return;
if(id == CHARTEVENT_MOUSE_MOVE)
{
TkMouse((int)lparam, (int)dparam, (((int)StringToInteger(sparam)) & 1) == 1);
return;
}
if(id == CHARTEVENT_OBJECT_CLICK)
{
// panel button (its text or its box)
if(StringFind(sparam, TK_PANEL + "L") == 0 || StringFind(sparam, TK_PANEL + "B") == 0)
{
int idx = (int)StringToInteger(StringSubstr(sparam, StringLen(TK_PANEL) + 1));
if(idx >= 0 && idx < g_nS && g_sB[idx] > BTN_GRIP) TkButton(g_sB[idx]);
return;
}
if(sparam == TK_RR_ENTRY || sparam == TK_RR_SL || sparam == TK_RR_TP) TkKeepSelected(sparam);
return;
}
if(id == CHARTEVENT_CHART_CHANGE)
{
// Check the background color change inside TkRefresh every time (so we do not rebuild on every scroll)
// The background color is checked inside TkRefresh (no rebuild on every scroll)
TkRefresh(false);
return;
}
if(id == CHARTEVENT_OBJECT_DRAG || id == CHARTEVENT_OBJECT_CHANGE)
{
int bit = (sparam == TK_RR_ENTRY) ? 1 : (sparam == TK_RR_SL) ? 2 : (sparam == TK_RR_TP) ? 4 : 0;
if(bit == 0) return;
// a dragged line snaps to the price step and stays
TkSetLine(sparam, ObjectGetDouble(0, sparam, OBJPROP_PRICE));
g_mv |= bit;
TkSaveMoved();
// dismiss the guide once the stop line has been moved
if(bit == 2 && !g_guideOff) { g_guideOff = true; TkSaveSettings(); }
// When the target line is moved by hand, clear the ratio selection and fix it there (only the ratio number changes)
// Target moved by hand: deselect the ratio and keep it there (only the ratio number changes)
if(bit == 4 && g_rrSel != 0) { g_rrSel = 0; TkSaveSettings(); }
TkRefresh(false);
}
}
//+------------------------------------------------------------------+