| //+------------------------------------------------------------------+
|
| //| 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);
|
| }
|
| }
|
| //+------------------------------------------------------------------+
|