| | """
|
| | Script 2 β Gap-Filled Microsecond Bid-Ask Unit Data Visualization
|
| | Fetches the SAME XAUUSDc data as Script 1, then fills every missing
|
| | price level between consecutive data points so that the Y-distribution
|
| | histogram reflects the full path traversed, not just the endpoints.
|
| |
|
| | Output: 4-panel figure identical in layout to Script 1 but built on the
|
| | gap-filled DataFrame.
|
| |
|
| | 0.01 unit = $0.01 XAU price change.
|
| | The 'c' suffix in XAUUSDc is an Exness broker account-type indicator
|
| | (standard cent live account), not related to XAU pricing.
|
| | """
|
| |
|
| | import MetaTrader5 as mt5
|
| | import pandas as pd
|
| | import numpy as np
|
| | import matplotlib
|
| | matplotlib.use('Agg')
|
| | import matplotlib.pyplot as plt
|
| | import matplotlib.dates as mdates
|
| | from datetime import datetime, timezone
|
| |
|
| |
|
| |
|
| |
|
| | if not mt5.initialize():
|
| | print(f"MT5 initialize() failed, error code = {mt5.last_error()}")
|
| | quit()
|
| |
|
| |
|
| |
|
| |
|
| | utc_from = datetime(2026, 2, 12, 0, 0, 0, tzinfo=timezone.utc)
|
| | utc_to = datetime(2026, 2, 12, 23, 59, 59, tzinfo=timezone.utc)
|
| |
|
| | SYMBOL = "XAUUSDc"
|
| | UNIT_SIZE = 0.01
|
| |
|
| |
|
| |
|
| |
|
| | ticks = mt5.copy_ticks_range(SYMBOL, utc_from, utc_to, mt5.COPY_TICKS_ALL)
|
| |
|
| | if ticks is None or len(ticks) == 0:
|
| | print(f"No data retrieved for {SYMBOL}. Error: {mt5.last_error()}")
|
| | mt5.shutdown()
|
| | quit()
|
| |
|
| | df = pd.DataFrame(ticks)
|
| | df['datetime'] = pd.to_datetime(df['time_msc'], unit='ms', utc=True)
|
| |
|
| | print(f"Fetched {len(df):,} raw data points for {SYMBOL}")
|
| | mt5.shutdown()
|
| |
|
| |
|
| | csv_raw = "raw_ticks_XAUUSDc_20260212.csv"
|
| | df[['datetime', 'bid', 'ask', 'last', 'volume', 'flags']].to_csv(csv_raw, index=False)
|
| | print(f"Saved CSV β {csv_raw} ({len(df):,} rows)")
|
| |
|
| |
|
| |
|
| |
|
| | def fill_gaps(prices: np.ndarray, timestamps_ns: np.ndarray, unit_size: float):
|
| | """
|
| | Vectorised gap-fill: for every consecutive pair (A β B),
|
| | insert intermediate price levels at every unit_size step.
|
| | timestamps_ns should be int64 nanoseconds.
|
| | """
|
| | diff_units = np.round(np.diff(prices) / unit_size).astype(np.int64)
|
| | counts = np.abs(diff_units)
|
| |
|
| | counts = np.append(counts, 1)
|
| |
|
| | total = int(np.sum(counts))
|
| | indices = np.repeat(np.arange(len(prices)), counts)
|
| |
|
| |
|
| | cum = np.cumsum(counts)
|
| | starts = np.empty_like(cum)
|
| | starts[0] = 0
|
| | starts[1:] = cum[:-1]
|
| | offsets = np.arange(total) - np.repeat(starts, counts)
|
| |
|
| |
|
| | directions = np.zeros(len(prices), dtype=np.float64)
|
| | directions[:-1] = np.sign(diff_units)
|
| |
|
| |
|
| | dt = np.zeros(len(prices), dtype=np.float64)
|
| | dt[:-1] = np.diff(timestamps_ns).astype(np.float64)
|
| | steps = dt / np.where(counts > 0, counts, 1)
|
| |
|
| | filled_prices = prices[indices] + offsets * directions[indices] * unit_size
|
| | filled_ts = timestamps_ns[indices].astype(np.float64) + offsets * steps[indices]
|
| |
|
| | return np.round(filled_prices, 2), filled_ts.astype(np.int64)
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | ts_ns = df['datetime'].values.astype('datetime64[ns]').astype(np.int64)
|
| |
|
| | print("Gap-filling bid...")
|
| | bid_prices_filled, bid_ts_filled = fill_gaps(df['bid'].values, ts_ns, UNIT_SIZE)
|
| | print("Gap-filling ask...")
|
| | ask_prices_filled, ask_ts_filled = fill_gaps(df['ask'].values, ts_ns, UNIT_SIZE)
|
| |
|
| | print(f"Bid: {len(df):,} raw β {len(bid_prices_filled):,} filled rows")
|
| | print(f"Ask: {len(df):,} raw β {len(ask_prices_filled):,} filled rows")
|
| |
|
| |
|
| | bid_filled_df = pd.DataFrame({
|
| | 'datetime': pd.to_datetime(bid_ts_filled, unit='ns', utc=True),
|
| | 'bid_filled': bid_prices_filled,
|
| | })
|
| | bid_csv = "filled_bid_XAUUSDc_20260212.csv"
|
| | bid_filled_df.to_csv(bid_csv, index=False)
|
| | print(f"Saved CSV β {bid_csv} ({len(bid_filled_df):,} rows)")
|
| |
|
| | ask_filled_df = pd.DataFrame({
|
| | 'datetime': pd.to_datetime(ask_ts_filled, unit='ns', utc=True),
|
| | 'ask_filled': ask_prices_filled,
|
| | })
|
| | ask_csv = "filled_ask_XAUUSDc_20260212.csv"
|
| | ask_filled_df.to_csv(ask_csv, index=False)
|
| | print(f"Saved CSV β {ask_csv} ({len(ask_filled_df):,} rows)")
|
| |
|
| |
|
| |
|
| | _UNIX_EPOCH_MPLDATE = 719163.0
|
| | bid_times = bid_ts_filled / 1e9 / 86400.0 + _UNIX_EPOCH_MPLDATE
|
| | ask_times = ask_ts_filled / 1e9 / 86400.0 + _UNIX_EPOCH_MPLDATE
|
| |
|
| |
|
| |
|
| |
|
| | overall_min = min(bid_prices_filled.min(), ask_prices_filled.min())
|
| | overall_max = max(bid_prices_filled.max(), ask_prices_filled.max())
|
| |
|
| | bin_lo = np.floor(overall_min / UNIT_SIZE) * UNIT_SIZE - UNIT_SIZE
|
| | bin_hi = np.ceil(overall_max / UNIT_SIZE) * UNIT_SIZE + UNIT_SIZE
|
| | bins = np.round(np.arange(bin_lo, bin_hi + UNIT_SIZE, UNIT_SIZE), 2)
|
| |
|
| | print("Plotting...")
|
| |
|
| |
|
| |
|
| |
|
| | fig, axes = plt.subplots(
|
| | 2, 2,
|
| | figsize=(20, 12),
|
| | gridspec_kw={'width_ratios': [1, 4]},
|
| | sharey='row',
|
| | )
|
| | fig.suptitle(
|
| | f'{SYMBOL} β Gap-Filled Unit Data (Path-Weighted) | {utc_from.strftime("%Y-%m-%d")}',
|
| | fontsize=16, fontweight='bold',
|
| | )
|
| |
|
| |
|
| | BID_COLOR = '#0000FF'
|
| | ASK_COLOR = '#FF0000'
|
| |
|
| |
|
| | ax_hist_bid = axes[0, 0]
|
| | ax_line_bid = axes[0, 1]
|
| |
|
| | ax_hist_bid.hist(
|
| | bid_prices_filled, bins=bins, orientation='horizontal',
|
| | color=BID_COLOR, alpha=1.0, edgecolor='white', linewidth=0.3,
|
| | )
|
| | ax_hist_bid.set_xlabel('Count (path-weighted)', fontsize=10)
|
| | ax_hist_bid.set_ylabel('Bid Price', fontsize=10)
|
| | ax_hist_bid.set_title('Bid Y-Distribution β Gap-Filled (0.01-unit bins)', fontsize=12)
|
| |
|
| |
|
| |
|
| | ax_line_bid.plot(
|
| | bid_times, bid_prices_filled,
|
| | color=BID_COLOR, linewidth=0.5, alpha=1.0,
|
| | rasterized=True,
|
| | )
|
| | ax_line_bid.xaxis_date()
|
| | ax_line_bid.set_title('Bid Price β Gap-Filled (Time Series)', fontsize=12)
|
| | ax_line_bid.set_xlabel('Time (UTC)', fontsize=10)
|
| | ax_line_bid.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
|
| | ax_line_bid.xaxis.set_major_locator(mdates.HourLocator(interval=2))
|
| | plt.setp(ax_line_bid.xaxis.get_majorticklabels(), rotation=45, ha='right')
|
| | ax_line_bid.grid(True, alpha=0.3)
|
| |
|
| |
|
| | ax_hist_ask = axes[1, 0]
|
| | ax_line_ask = axes[1, 1]
|
| |
|
| | ax_hist_ask.hist(
|
| | ask_prices_filled, bins=bins, orientation='horizontal',
|
| | color=ASK_COLOR, alpha=1.0, edgecolor='white', linewidth=0.3,
|
| | )
|
| | ax_hist_ask.set_xlabel('Count (path-weighted)', fontsize=10)
|
| | ax_hist_ask.set_ylabel('Ask Price', fontsize=10)
|
| | ax_hist_ask.set_title('Ask Y-Distribution β Gap-Filled (0.01-unit bins)', fontsize=12)
|
| |
|
| |
|
| | ax_line_ask.plot(
|
| | ask_times, ask_prices_filled,
|
| | color=ASK_COLOR, linewidth=0.5, alpha=1.0,
|
| | rasterized=True,
|
| | )
|
| | ax_line_ask.xaxis_date()
|
| | ax_line_ask.set_title('Ask Price β Gap-Filled (Time Series)', fontsize=12)
|
| | ax_line_ask.set_xlabel('Time (UTC)', fontsize=10)
|
| | ax_line_ask.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
|
| | ax_line_ask.xaxis.set_major_locator(mdates.HourLocator(interval=2))
|
| | plt.setp(ax_line_ask.xaxis.get_majorticklabels(), rotation=45, ha='right')
|
| | ax_line_ask.grid(True, alpha=0.3)
|
| |
|
| |
|
| | plt.tight_layout(rect=[0, 0, 1, 0.95])
|
| |
|
| | output_path = "filled_ticks_4panel.png"
|
| | fig.savefig(output_path, dpi=150, bbox_inches='tight')
|
| | print(f"Saved β {output_path}")
|
| |
|