コンテンツにスキップ

【検証#08】0.3÷0.1=2余り0.1? — MathModの罠

MathMod比較スクリプトの実行結果

MathMod比較スクリプトの実行結果

この記事の3行まとめ

  • 🔢 MathMod(0.3, 0.1) は0ではなく 0.1に近い値を返すことがある
  • 💻 原因はIEEE 754浮動小数点の丸め誤差(#01と同じ)
  • ✅ 対策:NormalizeDoubleまたは整数変換

はじめに

この記事は、「とあるMetaTraderの備忘秘録」様のブログ記事を検証・紹介するシリーズの第8弾です。

01で扱ったdouble型比較問題の別バリエーションです。

元ネタ

0.3 ÷ 0.1 = 2 余り 0.1
https://fai-fx.hatenadiary.org/entry/20100330/1269936609
(2010年3月30日 公開)

時代背景に関する注意

この記事の元となるブログ記事は2010年頃に公開されたものです。
MQL5や最新のコンパイラでは浮動小数点演算の挙動が一部改善されている可能性がありますが、IEEE 754の根本的な問題は変わりません。


問題提起

期待する結果

Text Only
0.3 ÷ 0.1 = 3 余り 0

これは小学校算数の問題です。

MQL5での実行結果

MQL
double remainder = MathMod(0.3, 0.1);
Print("余り = ", remainder);  // 期待: 0.0

実際の出力:

Text Only
余り = 0.09999999999999998

0ではない!

余りが 0.1に近い値 になっています。
これは重大なバグの原因になります。


なぜこうなるのか

IEEE 754の限界

コンピュータは2進数で計算しますが、0.1や0.3は2進数で正確に表現できません

Text Only
0.1(10進)= 0.0001100110011...(2進、循環小数)
0.3(10進)= 0.0100110011001...(2進、循環小数)

つまり、コンピュータの中では: - 0.1 ≈ 0.10000000000000001 - 0.3 ≈ 0.29999999999999999

MathModの計算

Text Only
0.29999... ÷ 0.10000... = 2.999...

整数部分 = 2、余り = 0.29999... - (2 × 0.10000...) ≈ 0.09999...


実際に影響するケース

ロットサイズ計算

MQL
// ロットステップ0.1の通貨ペアで
double desiredLot = 0.3;
double lotStep = 0.1;

// 0.1刻みに丸めたい
double remainder = MathMod(desiredLot, lotStep);
if(remainder != 0)  // ← これが常にtrueになる!
{
    // 意図しない処理が実行される
}

時間判定

MQL
// 5分ごとに処理したい
double minutes = 15.0;
if(MathMod(minutes, 5.0) == 0)  // ← 正しく動作しない可能性
{
    DoSomething();
}

MQL5での検証コード

MQL
//+------------------------------------------------------------------+
//|                                           MathMod_Trap.mq5        |
//| 「とあるMetaTraderの備忘秘録」様の記事を検証                        |
//| https://fai-fx.hatenadiary.org/entry/20100330/1269936609          |
//+------------------------------------------------------------------+
#property copyright "FXおもしろラボ"
#property link      "https://fx-omoshiro-lab.com/"
#property version   "1.00"

void OnStart()
{
    Print("=== MathMod の罠 検証 ===");
    Print("");

    // 問題のケース
    double dividend = 0.3;
    double divisor = 0.1;

    double remainder = MathMod(dividend, divisor);

    Print("--- 基本テスト ---");
    PrintFormat("%.1f ÷ %.1f の余り", dividend, divisor);
    PrintFormat("期待値: 0.0");
    PrintFormat("実際値: %.17f", remainder);
    PrintFormat("0と判定: %s", (remainder == 0) ? "Yes" : "No ★問題★");
    Print("");

    // 他の問題ケース
    Print("--- 他の問題ケース ---");
    TestMathMod(0.7, 0.1);
    TestMathMod(1.0, 0.1);
    TestMathMod(1.1, 0.1);
    TestMathMod(2.0, 0.2);
    Print("");

    // 回避策1: NormalizeDouble
    Print("--- 回避策1: NormalizeDouble ---");
    double fixed1 = NormalizeDouble(MathMod(0.3, 0.1), 8);
    PrintFormat("NormalizeDouble(MathMod(0.3, 0.1), 8) = %.8f", fixed1);
    PrintFormat("0と判定: %s", (fixed1 == 0) ? "Yes ✓" : "No");
    Print("");

    // 回避策2: 整数変換
    Print("--- 回避策2: 整数変換 ---");
    int d1 = (int)MathRound(0.3 * 10);  // 3
    int d2 = (int)MathRound(0.1 * 10);  // 1
    int fixed2 = d1 % d2;
    PrintFormat("(int)(0.3*10) %% (int)(0.1*10) = %d %% %d = %d", d1, d2, fixed2);
    PrintFormat("0と判定: %s", (fixed2 == 0) ? "Yes ✓" : "No");
    Print("");

    // 回避策3: 許容誤差での比較
    Print("--- 回避策3: 許容誤差での比較 ---");
    double epsilon = 0.0000001;
    bool isZero = MathAbs(remainder) < epsilon;
    PrintFormat("MathAbs(%.17f) < %.7f = %s", remainder, epsilon, isZero ? "Yes ✓" : "No");

    Print("");
    Print("=== テスト完了 ===");
}

void TestMathMod(double a, double b)
{
    double result = MathMod(a, b);
    string status = (MathAbs(result) < 0.0000001 || MathAbs(result - b) < 0.0000001) 
                    ? "OK" : "★問題★";
    PrintFormat("MathMod(%.1f, %.1f) = %.17f  %s", a, b, result, status);
}

回避策まとめ

方法1: NormalizeDouble(注意)

MQL
double remainder = NormalizeDouble(MathMod(value, step), 8);
if(remainder == 0) { /* OK */ }
注意: MathMod の結果が「割る数 (0.1)」に極めて近くなってしまった場合(例: 0.0999...)、NormalizeDoubleしても 0.1 に丸められるだけで 0にはなりません。この方法は万能ではありません。

方法2: 整数に変換して計算(推奨)

MQL
// 0.01単位なら100倍して整数化
int valueInt = (int)MathRound(value * 100);
int stepInt = (int)MathRound(step * 100);
int remainder = valueInt % stepInt;
if(remainder == 0)
{
    // OK
}

方法3: 許容誤差で比較

MQL
double remainder = MathMod(value, step);
double epsilon = 0.0000001;
// 0に近いか、または「補数が0に近い(=割る数に近い)」かを判定
if(MathAbs(remainder) < epsilon || MathAbs(remainder - step) < epsilon)
{
    // OK
}

推奨

整数変換が最も確実です。
特にロットサイズ計算では、SymbolInfoDouble で得た値を整数化して処理しましょう。


まとめ

ポイント 内容
問題 MathMod(0.3, 0.1) ≠ 0
原因 IEEE 754浮動小数点の丸め誤差
影響 ロット計算、時間判定などで誤動作
対策 整数変換、NormalizeDouble、許容誤差比較

オリジナル記事への謝辞

この記事は「とあるMetaTraderの備忘秘録」様の貴重な知見をもとに、
MQL5での検証と解説を加えたものです。
オリジナル記事に心より感謝いたします。


ソースコードのダウンロード

この記事で紹介したコードをダウンロードできます。

ファイルの種類:スクリプト

保存先: MQL5/Scripts/ フォルダ
使い方: MT5のナビゲーターから「スクリプト」を展開し、チャートにドラッグ&ドロップで実行

08_MathMod_Trap.mq5 をダウンロード


関連記事・用語