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