algorembrant commited on
Commit
c99df4c
·
verified ·
1 Parent(s): db3bccc

Upload 29 files

Browse files
.gitattributes CHANGED
@@ -1,35 +1,3 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.csv filter=lfs diff=lfs merge=lfs -text
2
+ images/filled_ticks_4panel.png filter=lfs diff=lfs merge=lfs -text
3
+ images/raw_ticks_4panel.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .venv
2
+ .env
IDEA.md ADDED
File without changes
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ContinualQuasars
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Python/mt5_filled_ticks.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Script 2 — Gap-Filled Microsecond Bid-Ask Unit Data Visualization
3
+ Fetches the SAME XAUUSDc data as Script 1, then fills every missing
4
+ price level between consecutive data points so that the Y-distribution
5
+ histogram reflects the full path traversed, not just the endpoints.
6
+
7
+ Output: 4-panel figure identical in layout to Script 1 but built on the
8
+ gap-filled DataFrame.
9
+
10
+ 0.01 unit = $0.01 XAU price change.
11
+ The 'c' suffix in XAUUSDc is an Exness broker account-type indicator
12
+ (standard cent live account), not related to XAU pricing.
13
+ """
14
+
15
+ import MetaTrader5 as mt5
16
+ import pandas as pd
17
+ import numpy as np
18
+ import matplotlib
19
+ matplotlib.use('Agg') # Headless backend — no GUI window
20
+ import matplotlib.pyplot as plt
21
+ import matplotlib.dates as mdates
22
+ from datetime import datetime, timezone
23
+
24
+ # ──────────────────────────────────────────────
25
+ # 1. Connect to MT5
26
+ # ──────────────────────────────────────────────
27
+ if not mt5.initialize():
28
+ print(f"MT5 initialize() failed, error code = {mt5.last_error()}")
29
+ quit()
30
+
31
+ # ──────────────────────────────────────────────
32
+ # 2. Define time range (Feb 12 2026, full day UTC)
33
+ # ──────────────────────────────────────────────
34
+ utc_from = datetime(2026, 2, 12, 0, 0, 0, tzinfo=timezone.utc)
35
+ utc_to = datetime(2026, 2, 12, 23, 59, 59, tzinfo=timezone.utc)
36
+
37
+ SYMBOL = "XAUUSDc"
38
+ UNIT_SIZE = 0.01 # the binsize (0.01 unit = $0.01 XAU price change)
39
+
40
+ # ──────────────────────────────────────────────
41
+ # 3. Fetch data from MT5 (same query as Script 1)
42
+ # ──────────────────────────────────────────────
43
+ ticks = mt5.copy_ticks_range(SYMBOL, utc_from, utc_to, mt5.COPY_TICKS_ALL)
44
+
45
+ if ticks is None or len(ticks) == 0:
46
+ print(f"No data retrieved for {SYMBOL}. Error: {mt5.last_error()}")
47
+ mt5.shutdown()
48
+ quit()
49
+
50
+ df = pd.DataFrame(ticks)
51
+ df['datetime'] = pd.to_datetime(df['time_msc'], unit='ms', utc=True)
52
+
53
+ print(f"Fetched {len(df):,} raw data points for {SYMBOL}")
54
+ mt5.shutdown()
55
+
56
+ # Save raw unit CSV (same data as Script 1)
57
+ csv_raw = "raw_ticks_XAUUSDc_20260212.csv" # filename kept for compatibility
58
+ df[['datetime', 'bid', 'ask', 'last', 'volume', 'flags']].to_csv(csv_raw, index=False)
59
+ print(f"Saved CSV → {csv_raw} ({len(df):,} rows)")
60
+
61
+ # ──────────────────────────────────────────────
62
+ # 4. Vectorised gap-filling function
63
+ # ──────────────────────────────────────────────
64
+ def fill_gaps(prices: np.ndarray, timestamps_ns: np.ndarray, unit_size: float):
65
+ """
66
+ Vectorised gap-fill: for every consecutive pair (A → B),
67
+ insert intermediate price levels at every unit_size step.
68
+ timestamps_ns should be int64 nanoseconds.
69
+ """
70
+ diff_units = np.round(np.diff(prices) / unit_size).astype(np.int64)
71
+ counts = np.abs(diff_units)
72
+ # Last point gets a count of 1 (just itself)
73
+ counts = np.append(counts, 1)
74
+
75
+ total = int(np.sum(counts))
76
+ indices = np.repeat(np.arange(len(prices)), counts)
77
+
78
+ # Offset within each segment
79
+ cum = np.cumsum(counts)
80
+ starts = np.empty_like(cum)
81
+ starts[0] = 0
82
+ starts[1:] = cum[:-1]
83
+ offsets = np.arange(total) - np.repeat(starts, counts)
84
+
85
+ # Direction per segment
86
+ directions = np.zeros(len(prices), dtype=np.float64)
87
+ directions[:-1] = np.sign(diff_units)
88
+
89
+ # Time step per segment
90
+ dt = np.zeros(len(prices), dtype=np.float64)
91
+ dt[:-1] = np.diff(timestamps_ns).astype(np.float64)
92
+ steps = dt / np.where(counts > 0, counts, 1)
93
+
94
+ filled_prices = prices[indices] + offsets * directions[indices] * unit_size
95
+ filled_ts = timestamps_ns[indices].astype(np.float64) + offsets * steps[indices]
96
+
97
+ return np.round(filled_prices, 2), filled_ts.astype(np.int64)
98
+
99
+
100
+ # ──────────────────────────────────────────────
101
+ # 5. Apply gap-filling to bid and ask separately
102
+ # ──────────────────────────────────────────────
103
+ ts_ns = df['datetime'].values.astype('datetime64[ns]').astype(np.int64)
104
+
105
+ print("Gap-filling bid...")
106
+ bid_prices_filled, bid_ts_filled = fill_gaps(df['bid'].values, ts_ns, UNIT_SIZE)
107
+ print("Gap-filling ask...")
108
+ ask_prices_filled, ask_ts_filled = fill_gaps(df['ask'].values, ts_ns, UNIT_SIZE)
109
+
110
+ print(f"Bid: {len(df):,} raw → {len(bid_prices_filled):,} filled rows")
111
+ print(f"Ask: {len(df):,} raw → {len(ask_prices_filled):,} filled rows")
112
+
113
+ # Save gap-filled CSVs
114
+ bid_filled_df = pd.DataFrame({
115
+ 'datetime': pd.to_datetime(bid_ts_filled, unit='ns', utc=True),
116
+ 'bid_filled': bid_prices_filled,
117
+ })
118
+ bid_csv = "filled_bid_XAUUSDc_20260212.csv"
119
+ bid_filled_df.to_csv(bid_csv, index=False)
120
+ print(f"Saved CSV → {bid_csv} ({len(bid_filled_df):,} rows)")
121
+
122
+ ask_filled_df = pd.DataFrame({
123
+ 'datetime': pd.to_datetime(ask_ts_filled, unit='ns', utc=True),
124
+ 'ask_filled': ask_prices_filled,
125
+ })
126
+ ask_csv = "filled_ask_XAUUSDc_20260212.csv"
127
+ ask_filled_df.to_csv(ask_csv, index=False)
128
+ print(f"Saved CSV → {ask_csv} ({len(ask_filled_df):,} rows)")
129
+
130
+ # Convert ns timestamps to matplotlib date floats
131
+ # matplotlib dates = days since 0001-01-01; Unix epoch = day 719163
132
+ _UNIX_EPOCH_MPLDATE = 719163.0
133
+ bid_times = bid_ts_filled / 1e9 / 86400.0 + _UNIX_EPOCH_MPLDATE
134
+ ask_times = ask_ts_filled / 1e9 / 86400.0 + _UNIX_EPOCH_MPLDATE
135
+
136
+ # ──────────────────────────────────────────────
137
+ # 6. Build histogram bins (1 bin = 0.01 unit)
138
+ # ──────────────────────────────────────────────
139
+ overall_min = min(bid_prices_filled.min(), ask_prices_filled.min())
140
+ overall_max = max(bid_prices_filled.max(), ask_prices_filled.max())
141
+
142
+ bin_lo = np.floor(overall_min / UNIT_SIZE) * UNIT_SIZE - UNIT_SIZE
143
+ bin_hi = np.ceil(overall_max / UNIT_SIZE) * UNIT_SIZE + UNIT_SIZE
144
+ bins = np.round(np.arange(bin_lo, bin_hi + UNIT_SIZE, UNIT_SIZE), 2)
145
+
146
+ print("Plotting...")
147
+
148
+ # ──────────────────────────────────────────────
149
+ # 7. Plot 4-panel figure
150
+ # ──────────────────────────────────────────────
151
+ fig, axes = plt.subplots(
152
+ 2, 2,
153
+ figsize=(20, 12),
154
+ gridspec_kw={'width_ratios': [1, 4]},
155
+ sharey='row',
156
+ )
157
+ fig.suptitle(
158
+ f'{SYMBOL} — Gap-Filled Unit Data (Path-Weighted) | {utc_from.strftime("%Y-%m-%d")}',
159
+ fontsize=16, fontweight='bold',
160
+ )
161
+
162
+ # Colors — 100% blue and 100% red per IDEA.md
163
+ BID_COLOR = '#0000FF'
164
+ ASK_COLOR = '#FF0000'
165
+
166
+ # ── Row 0: BID ─────────────────────────────
167
+ ax_hist_bid = axes[0, 0]
168
+ ax_line_bid = axes[0, 1]
169
+
170
+ ax_hist_bid.hist(
171
+ bid_prices_filled, bins=bins, orientation='horizontal',
172
+ color=BID_COLOR, alpha=1.0, edgecolor='white', linewidth=0.3,
173
+ )
174
+ ax_hist_bid.set_xlabel('Count (path-weighted)', fontsize=10)
175
+ ax_hist_bid.set_ylabel('Bid Price', fontsize=10)
176
+ ax_hist_bid.set_title('Bid Y-Distribution — Gap-Filled (0.01-unit bins)', fontsize=12)
177
+ # histogram grows left-to-right (starts from 0)
178
+
179
+ # Line only — no markers for 4M+ points, rasterized
180
+ ax_line_bid.plot(
181
+ bid_times, bid_prices_filled,
182
+ color=BID_COLOR, linewidth=0.5, alpha=1.0,
183
+ rasterized=True,
184
+ )
185
+ ax_line_bid.xaxis_date()
186
+ ax_line_bid.set_title('Bid Price — Gap-Filled (Time Series)', fontsize=12)
187
+ ax_line_bid.set_xlabel('Time (UTC)', fontsize=10)
188
+ ax_line_bid.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
189
+ ax_line_bid.xaxis.set_major_locator(mdates.HourLocator(interval=2))
190
+ plt.setp(ax_line_bid.xaxis.get_majorticklabels(), rotation=45, ha='right')
191
+ ax_line_bid.grid(True, alpha=0.3)
192
+
193
+ # ── Row 1: ASK ─────────────────────────────
194
+ ax_hist_ask = axes[1, 0]
195
+ ax_line_ask = axes[1, 1]
196
+
197
+ ax_hist_ask.hist(
198
+ ask_prices_filled, bins=bins, orientation='horizontal',
199
+ color=ASK_COLOR, alpha=1.0, edgecolor='white', linewidth=0.3,
200
+ )
201
+ ax_hist_ask.set_xlabel('Count (path-weighted)', fontsize=10)
202
+ ax_hist_ask.set_ylabel('Ask Price', fontsize=10)
203
+ ax_hist_ask.set_title('Ask Y-Distribution — Gap-Filled (0.01-unit bins)', fontsize=12)
204
+ # histogram grows left-to-right (starts from 0)
205
+
206
+ ax_line_ask.plot(
207
+ ask_times, ask_prices_filled,
208
+ color=ASK_COLOR, linewidth=0.5, alpha=1.0,
209
+ rasterized=True,
210
+ )
211
+ ax_line_ask.xaxis_date()
212
+ ax_line_ask.set_title('Ask Price — Gap-Filled (Time Series)', fontsize=12)
213
+ ax_line_ask.set_xlabel('Time (UTC)', fontsize=10)
214
+ ax_line_ask.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
215
+ ax_line_ask.xaxis.set_major_locator(mdates.HourLocator(interval=2))
216
+ plt.setp(ax_line_ask.xaxis.get_majorticklabels(), rotation=45, ha='right')
217
+ ax_line_ask.grid(True, alpha=0.3)
218
+
219
+ # ── Final layout ───────────────────────────
220
+ plt.tight_layout(rect=[0, 0, 1, 0.95])
221
+
222
+ output_path = "filled_ticks_4panel.png"
223
+ fig.savefig(output_path, dpi=150, bbox_inches='tight')
224
+ print(f"Saved → {output_path}")
Python/mt5_raw_ticks.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Script 1 — Raw Microsecond Bid-Ask Unit Data Visualization
3
+ Fetches XAUUSDc data from MetaTrader 5 for February 12, 2026 (full day),
4
+ and produces a 4-panel figure:
5
+ Top-left: Bid Y-distribution histogram (blue, 0.01-unit bins)
6
+ Top-right: Bid line chart with dot markers (blue)
7
+ Bottom-left: Ask Y-distribution histogram (red, 0.01-unit bins)
8
+ Bottom-right:Ask line chart with dot markers (red)
9
+
10
+ 0.01 unit = $0.01 XAU price change.
11
+ The 'c' suffix in XAUUSDc is an Exness broker account-type indicator
12
+ (standard cent live account), not related to XAU pricing.
13
+ """
14
+
15
+ import MetaTrader5 as mt5
16
+ import pandas as pd
17
+ import numpy as np
18
+ import matplotlib
19
+ matplotlib.use('Agg') # Headless backend — no GUI window
20
+ import matplotlib.pyplot as plt
21
+ import matplotlib.dates as mdates
22
+ from datetime import datetime, timezone
23
+
24
+ # ──────────────────────────────────────────────
25
+ # 1. Connect to MT5
26
+ # ──────────────────────────────────────────────
27
+ if not mt5.initialize():
28
+ print(f"MT5 initialize() failed, error code = {mt5.last_error()}")
29
+ quit()
30
+
31
+ # ──────────────────────────────────────────────
32
+ # 2. Define time range (Feb 12 2026, full day UTC)
33
+ # ──────────────────────────────────────────────
34
+ utc_from = datetime(2026, 2, 12, 0, 0, 0, tzinfo=timezone.utc)
35
+ utc_to = datetime(2026, 2, 12, 23, 59, 59, tzinfo=timezone.utc)
36
+
37
+ SYMBOL = "XAUUSDc"
38
+ UNIT_SIZE = 0.01 # the binsize (0.01 unit = $0.01 XAU price change)
39
+
40
+ # ──────────────────────────────────────────────
41
+ # 3. Fetch data from MT5
42
+ # ──────────────────────────────────────────────
43
+ ticks = mt5.copy_ticks_range(SYMBOL, utc_from, utc_to, mt5.COPY_TICKS_ALL)
44
+
45
+ if ticks is None or len(ticks) == 0:
46
+ print(f"No data retrieved for {SYMBOL}. Error: {mt5.last_error()}")
47
+ mt5.shutdown()
48
+ quit()
49
+
50
+ df = pd.DataFrame(ticks)
51
+ # MT5 returns time in seconds since epoch; time_msc is milliseconds
52
+ df['datetime'] = pd.to_datetime(df['time_msc'], unit='ms', utc=True)
53
+
54
+ print(f"Fetched {len(df):,} data points for {SYMBOL}")
55
+ print(f"Time range: {df['datetime'].iloc[0]} → {df['datetime'].iloc[-1]}")
56
+ print(f"Bid range : {df['bid'].min():.2f} – {df['bid'].max():.2f}")
57
+ print(f"Ask range : {df['ask'].min():.2f} – {df['ask'].max():.2f}")
58
+
59
+ mt5.shutdown()
60
+
61
+ # ──────────────────────────────────────────────
62
+ # 3b. Save raw unit data to CSV
63
+ # ──────────────────────────────────────────────
64
+ csv_path = "raw_ticks_XAUUSDc_20260212.csv"
65
+ df[['datetime', 'bid', 'ask', 'last', 'volume', 'flags']].to_csv(csv_path, index=False)
66
+ print(f"Saved CSV → {csv_path} ({len(df):,} rows)")
67
+
68
+ # ──────────────────────────────────────────────
69
+ # 4. Build histogram bins (1 bin = 0.01 unit)
70
+ # ──────────────────────────────────────────────
71
+ overall_min = min(df['bid'].min(), df['ask'].min())
72
+ overall_max = max(df['bid'].max(), df['ask'].max())
73
+
74
+ bin_lo = np.floor(overall_min / UNIT_SIZE) * UNIT_SIZE - UNIT_SIZE
75
+ bin_hi = np.ceil(overall_max / UNIT_SIZE) * UNIT_SIZE + UNIT_SIZE
76
+ bins = np.arange(bin_lo, bin_hi + UNIT_SIZE, UNIT_SIZE)
77
+ bins = np.round(bins, 2)
78
+
79
+ # ──────────────────────────────────────────────
80
+ # 5. Convert datetimes to float (much faster for plotting)
81
+ # ──────────────────────────────────────────────
82
+ bid_times = mdates.date2num(df['datetime'].values)
83
+ ask_times = bid_times # same timestamps
84
+
85
+ print("Plotting...")
86
+
87
+ # ──────────────────────────────────────────────
88
+ # 6. Plot 4-panel figure
89
+ # ──────────────────────────────────────────────
90
+ fig, axes = plt.subplots(
91
+ 2, 2,
92
+ figsize=(20, 12),
93
+ gridspec_kw={'width_ratios': [1, 4]},
94
+ sharey='row',
95
+ )
96
+ fig.suptitle(
97
+ f'{SYMBOL} — Raw Microsecond Unit Data | {utc_from.strftime("%Y-%m-%d")}',
98
+ fontsize=16, fontweight='bold',
99
+ )
100
+
101
+ # Colors — 100% blue and 100% red per IDEA.md
102
+ BID_COLOR = '#0000FF'
103
+ ASK_COLOR = '#FF0000'
104
+
105
+ # ── Row 0: BID ─────────────────────────────
106
+ ax_hist_bid = axes[0, 0]
107
+ ax_line_bid = axes[0, 1]
108
+
109
+ # Histogram (horizontal)
110
+ ax_hist_bid.hist(
111
+ df['bid'].values, bins=bins, orientation='horizontal',
112
+ color=BID_COLOR, alpha=1.0, edgecolor='white', linewidth=0.3,
113
+ )
114
+ ax_hist_bid.set_xlabel('Count', fontsize=10)
115
+ ax_hist_bid.set_ylabel('Bid Price', fontsize=10)
116
+ ax_hist_bid.set_title('Bid Y-Distribution (0.01-unit bins)', fontsize=12)
117
+ # histogram grows left-to-right (starts from 0)
118
+
119
+ # Line chart — use line only (no markers) for massive data, rasterized
120
+ ax_line_bid.plot(
121
+ bid_times, df['bid'].values,
122
+ color=BID_COLOR, linewidth=0.5, alpha=1.0,
123
+ rasterized=True,
124
+ )
125
+ ax_line_bid.xaxis_date()
126
+ ax_line_bid.set_title('Bid Price (Time Series)', fontsize=12)
127
+ ax_line_bid.set_xlabel('Time (UTC)', fontsize=10)
128
+ ax_line_bid.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
129
+ ax_line_bid.xaxis.set_major_locator(mdates.HourLocator(interval=2))
130
+ plt.setp(ax_line_bid.xaxis.get_majorticklabels(), rotation=45, ha='right')
131
+ ax_line_bid.grid(True, alpha=0.3)
132
+
133
+ # ── Row 1: ASK ─────────────────────────────
134
+ ax_hist_ask = axes[1, 0]
135
+ ax_line_ask = axes[1, 1]
136
+
137
+ # Histogram (horizontal)
138
+ ax_hist_ask.hist(
139
+ df['ask'].values, bins=bins, orientation='horizontal',
140
+ color=ASK_COLOR, alpha=1.0, edgecolor='white', linewidth=0.3,
141
+ )
142
+ ax_hist_ask.set_xlabel('Count', fontsize=10)
143
+ ax_hist_ask.set_ylabel('Ask Price', fontsize=10)
144
+ ax_hist_ask.set_title('Ask Y-Distribution (0.01-unit bins)', fontsize=12)
145
+ # histogram grows left-to-right (starts from 0)
146
+
147
+ # Line chart — line only, rasterized
148
+ ax_line_ask.plot(
149
+ ask_times, df['ask'].values,
150
+ color=ASK_COLOR, linewidth=0.5, alpha=1.0,
151
+ rasterized=True,
152
+ )
153
+ ax_line_ask.xaxis_date()
154
+ ax_line_ask.set_title('Ask Price (Time Series)', fontsize=12)
155
+ ax_line_ask.set_xlabel('Time (UTC)', fontsize=10)
156
+ ax_line_ask.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
157
+ ax_line_ask.xaxis.set_major_locator(mdates.HourLocator(interval=2))
158
+ plt.setp(ax_line_ask.xaxis.get_majorticklabels(), rotation=45, ha='right')
159
+ ax_line_ask.grid(True, alpha=0.3)
160
+
161
+ # ── Final layout ───────────────────────────
162
+ plt.tight_layout(rect=[0, 0, 1, 0.95])
163
+
164
+ output_path = "raw_ticks_4panel.png"
165
+ fig.savefig(output_path, dpi=150, bbox_inches='tight')
166
+ print(f"Saved → {output_path}")
README.md ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # What Happens if We Increase or Decrease the Bin Size of Market Profiles (TPO & Volume)?
2
+
3
+ This applies specifically to the standard or free-tier market profiles available on most charting platforms. Market profiles are typically built on either the **Time Price Opportunity (TPO)** profile or the **Volume Profile (VP)**, whether real or tick-based. Regardless of the type, the underlying calculation is the same: raw data is cleaned through dataframes (xlsx, csv, etc.) and represented through graphs (lines, bars, plots, etc.). A market profile is simply datapoints collapsed into a y-axis distribution, forming a "profile." That is it -- nothing more.
4
+
5
+ But what actually happens when we increase or decrease the **bin size** (the price-step) of the market profile?
6
+
7
+ <br><br>
8
+
9
+ ## Collapsing Price Action into a Profile
10
+
11
+ <table>
12
+ <tr>
13
+ <td align="center">
14
+ <img width="995" height="488" alt="Collapsing price action into a market profile" src="https://github.com/user-attachments/assets/30f8a35e-1060-48c2-8f63-bbda3b989c93" />
15
+ </td>
16
+ </tr>
17
+ </table>
18
+
19
+ In the figure above, we have one MP chart (left) and one line chart (right), both derived from the same dataset. The price action over time (line chart) moves from point **A** to **B**, **C**, and **D**. When we collapse those datapoints (A through D) into a y-axis distribution histogram, a Market Profile chart is formed.
20
+
21
+ If we use a bin size of **1.000** and the price range spans **3000.000 to 3010.000**, then between those prices we get **10 bins** worth of grouping. Same data, different representation.
22
+
23
+ <br><br>
24
+
25
+ ## Larger Datasets and Stacking
26
+
27
+ <table>
28
+ <tr>
29
+ <td align="center">
30
+ <img width="996" height="488" alt="Larger dataset forming a market profile" src="https://github.com/user-attachments/assets/f05cdd33-80a0-47e1-8041-56543ef5d7d5" />
31
+ </td>
32
+ </tr>
33
+ </table>
34
+
35
+ With larger datasets, the principle remains the same. Datapoints collapse and stack to form a y-distribution. The more stacking occurs, the larger the profile becomes. In this example, the lowest profile value is **0** and the largest profile value is **4 stacks**.
36
+
37
+ <br><br>
38
+
39
+ ## The Effect of Changing Bin Size
40
+
41
+ <table>
42
+ <tr>
43
+ <td align="center">
44
+ <img width="995" height="484" alt="Market profile with different bin size" src="https://github.com/user-attachments/assets/6637ea25-0abc-40ad-a8a1-b35d2abd89cb" />
45
+ </td>
46
+ </tr>
47
+ </table>
48
+
49
+ The figure above uses the **same dataset** as the previous one, yet the profile looks different. It now has a lowest value of **0** and a largest value of only **2 stacks**. If you are a beginner, this might feel suspicious when experimenting on your preferred charting platform -- but it is completely normal.
50
+
51
+ The reason is straightforward: **more bins means more price groups for the datapoints to distribute across.** As bin size decreases (more granular bins), each datapoint lands in a more specific price bucket. This spreads the data across more bins, resulting in shorter stacks and a flatter profile. Conversely, increasing the bin size consolidates datapoints into fewer groups, producing taller stacks and a more concentrated profile.
52
+
53
+ That is the kind of market profile you typically get on free-tier charting and trading platforms.
54
+
55
+ <br><br>
56
+
57
+ ## An Alternative Approach: Trail-Price Clustering
58
+
59
+ There is another way to model a market profile. Without going into full detail -- if you are an algorithmic trader or software developer familiar with feature engineering, this will be straightforward. The core idea is to **add data to your original dataframes by clustering trail-prices** (an original concept) to produce a more complete set of datapoints.
60
+
61
+ <table>
62
+ <tr>
63
+ <td align="center">
64
+ <img width="1006" height="490" alt="Trail-price clustering concept" src="https://github.com/user-attachments/assets/8277ff21-9999-4ac1-b9db-e7c8b99578a3" />
65
+ </td>
66
+ </tr>
67
+ </table>
68
+
69
+ This concept extends well beyond these illustrations. You can fill in missing data in dataframes (for any dataset) by applying a custom formula using your preferred programming language.
70
+
71
+ <br><br>
72
+
73
+ <table>
74
+ <tr>
75
+ <td align="center">
76
+ <img width="1066" height="519" alt="Enhanced market profile model" src="https://github.com/user-attachments/assets/e2bcdea8-1944-4c4f-a467-1d8943972512" />
77
+ </td>
78
+ </tr>
79
+ </table>
80
+
81
+ The drawings may be rough, but the point stands. In our case, we model a market profile not based on **TOCHL** (Time, Open, Close, High, Low) or **Volume** (real, tick) but on **mBA** (microsecond raw bid/ask) formation.
82
+
83
+ <br><br>
84
+
85
+ ## Reference
86
+
87
+ ```bibtex
88
+ @misc{continualquasars2026blog1,
89
+ title = {What Happens if We Increase or Decrease the Bin Size of Market Profiles (TPO \& Volume)?},
90
+ author = {ContinualQuasars},
91
+ year = {2026},
92
+ url = {https://github.com/ContinualQuasars/BLOG-1},
93
+ note = {What Happens if We Increase or Decrease the Bin Size of Market Profiles (TPO \& Volume)?}
94
+ }
95
+ ```
STRUCTURE.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Project Structure
2
+
3
+ ```text
4
+ mBA-Terminal/
5
+ ├── images/
6
+ │ ├── filled_ticks_4panel.png
7
+ │ └── raw_ticks_4panel.png
8
+ ├── output/
9
+ │ ├── filled_ask_XAUUSDc_20260212.csv
10
+ │ ├── filled_bid_XAUUSDc_20260212.csv
11
+ │ └── raw_ticks_XAUUSDc_20260212.csv
12
+ ├── Python/
13
+ │ ├── mt5_filled_ticks.py
14
+ │ └── mt5_raw_ticks.py
15
+ ├── scripts/
16
+ │ └── debug_mt5.py
17
+ ├── src/
18
+ │ ├── core/
19
+ │ │ ├── data_worker.py
20
+ │ │ ├── market_profile.py
21
+ │ │ └── mt5_interface.py
22
+ │ ├── ui/
23
+ │ │ ├── chart_widget.py
24
+ │ │ ├── control_panel.py
25
+ │ │ └── main_window.py
26
+ │ ├── config.py
27
+ │ └── main.py
28
+ ├── tests/
29
+ │ ├── test_interactive_chart.py
30
+ │ ├── test_logic.py
31
+ │ ├── test_profile_logic.py
32
+ │ └── verify_levels.py
33
+ ├── .gitattributes
34
+ ├── .gitignore
35
+ ├── IDEA.md
36
+ ├── LICENSE
37
+ ├── market_profile_paper.tex
38
+ ├── README.md
39
+ ├── requirements.txt
40
+ └── TECHSTACK.md
41
+ ```
TECHSTACK.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Techstack
2
+
3
+ Audit of **mBA-Terminal** project files (excluding environment and cache):
4
+
5
+ | File Type | Count | Size (KB) |
6
+ | :--- | :--- | :--- |
7
+ | Python (.py) | 15 | 62.1 |
8
+ | (no extension) | 3 | 1.1 |
9
+ | CSV (.csv) | 3 | 426,797.2 |
10
+ | Markdown (.md) | 2 | 4.9 |
11
+ | PNG Image (.png) | 2 | 482.2 |
12
+ | LaTeX (.tex) | 1 | 31.3 |
13
+ | Plain Text (.txt) | 1 | 0.1 |
14
+ | **Total** | **27** | **427,379.1** |
images/filled_ticks_4panel.png ADDED

Git LFS Details

  • SHA256: 81c0bd94b80c4f3c5891c8fb725a06333ad3f1a75450681e80b9a2cdcbc1ca24
  • Pointer size: 131 Bytes
  • Size of remote file: 252 kB
images/raw_ticks_4panel.png ADDED

Git LFS Details

  • SHA256: e2b209e52d8d0a775ea5e8175d5e072150f334618a4beb8df445440ab015f8b0
  • Pointer size: 131 Bytes
  • Size of remote file: 242 kB
market_profile_paper.tex ADDED
@@ -0,0 +1,730 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ \documentclass[conference]{IEEEtran}
2
+
3
+ % ─── Packages ───────────────────────────────────────────
4
+ \usepackage[utf8]{inputenc}
5
+ \usepackage[T1]{fontenc}
6
+ \usepackage{amsmath, amssymb, amsfonts}
7
+ \usepackage{graphicx}
8
+ \usepackage{booktabs}
9
+ \usepackage{hyperref}
10
+ \usepackage{float}
11
+ \usepackage{caption}
12
+ \usepackage{subcaption}
13
+ \usepackage{xcolor}
14
+ \usepackage{enumitem}
15
+ \usepackage{cite}
16
+ \usepackage{array}
17
+ \usepackage{url}
18
+
19
+ \hypersetup{
20
+ colorlinks=true,
21
+ linkcolor=blue!70!black,
22
+ citecolor=blue!70!black,
23
+ urlcolor=blue!60!black,
24
+ }
25
+
26
+ % ─── Title ──────────────────────────────────────────────
27
+ \title{mBA-Profile: Market Profile Construction from Microsecond Bid-Ask Unit Data Using The Path-weighted Gap-filling Approach}
28
+
29
+ \author{
30
+ \IEEEauthorblockN{
31
+ Rembrant Oyangoren Albeos~%
32
+ \href{https://orcid.org/0009-0006-8743-4419}{%
33
+ \includegraphics[height=8pt]{ORCID_icon.png}%
34
+ }%
35
+ \textsuperscript{\hyperref[sec:author_info]{$\dagger$}}
36
+ }
37
+ \IEEEauthorblockA{%
38
+ \includegraphics[height=7pt]{ContinualQuasars_icon.png}\hspace{0.4em}Continual Quasars\\
39
+ }
40
+ }
41
+
42
+ \begin{document}
43
+ \maketitle
44
+
45
+ % ════════════════════════════════════════════════════════
46
+ \begin{abstract}
47
+ Conventional market profile construction collapses raw price data
48
+ directly into a Y-distribution histogram, recording only the price
49
+ levels that were explicitly quoted by the exchange or broker feed.
50
+ This paper presents an alternative approach---termed
51
+ \emph{path-weighted gap-filling}---in which synthetic trail-datapoints
52
+ are inserted at every intermediate unit-level price between
53
+ consecutive observations, producing an extended dataset that yields a
54
+ substantially denser and more continuous market profile. The
55
+ modelling is grounded in microsecond-resolution raw bid/ask unit data
56
+ rather than aggregated TOHLC (time, open, high, low, close) bars or
57
+ volume figures, thereby preserving the highest available fidelity of
58
+ the underlying price process. We demonstrate the approach on a full
59
+ trading day of \texttt{XAUUSDc} data collected from a live trading
60
+ environment, and show that the gap-filled profile eliminates the empty
61
+ bins and sparse regions that afflict raw-unit profiles during fast
62
+ directional moves, producing a more representative picture of
63
+ intraday price dynamics. All resources and code used in this work are available on GitHub at \url{https://github.com/ContinualQuasars/mBA-Profile}.
64
+
65
+ \end{abstract}
66
+
67
+ \begin{IEEEkeywords}
68
+ Market profile, unit data, microsecond bid--ask, market microstructure, path-weighted, gap-filling.
69
+ \end{IEEEkeywords}
70
+
71
+ % ════════════════════════════════════════════════════════
72
+ \section{Introduction}
73
+ \label{sec:intro}
74
+
75
+ \subsection{Market Microstructure and Unit Data}
76
+
77
+ At the most granular level of market data, financial instruments are
78
+ quoted through discrete data updates: each update represents a change in
79
+ the best bid price, the best ask price, or both
80
+ simultaneously~\cite{hasbrouck2007,ohara1995}. Modern trading
81
+ platforms such as MetaTrader~5 (MT5) record these events with
82
+ millisecond-resolution timestamps, and the data is made available
83
+ through a Python API~\cite{mt5docs}.
84
+
85
+ The instrument studied in this paper is \texttt{XAUUSDc}, a
86
+ gold CFD (Contract for Difference) traded on a
87
+ standard cent live trading account provided by the Exness broker,
88
+ accessed through the MT5 platform. The \texttt{c} suffix in
89
+ \texttt{XAUUSDc} is an Exness broker account-type indicator
90
+ (denoting a standard cent live account) and has no bearing on the
91
+ XAU price data itself---extracting data from \texttt{XAUUSDc}
92
+ (cent account) or \texttt{XAUUSDm} (dollar account) yields the
93
+ same XAUUSD price data with three decimal places. The minimum price
94
+ increment for this instrument is exactly \$0.001.
95
+ Because the standard lot size for gold is 100 troy ounces, a single
96
+ price movement of \$0.001 corresponds to a profit-or-loss change of
97
+ \$0.10 per standard lot. In this study, the market profile
98
+ bin size is set to \$0.01 (one unit, where 0.01~unit = \$0.01 XAU
99
+ price change), to produce a more
100
+ stable and interpretable distribution.
101
+
102
+ \subsection{The Market Profile Concept}
103
+
104
+ A market profile is a rotated histogram of price over a defined time
105
+ window. The concept was introduced by J.~Peter Steidlmayer at the
106
+ Chicago Board of Trade in the 1980s~\cite{steidlmayer1986}.
107
+ Traditionally, a market profile uses 30-minute ``Time Price
108
+ Opportunity'' (TPO) letters stacked at each price level to show where
109
+ price spent the most time during a trading session~\cite{dalton2007}.
110
+ The horizontal axis represents frequency or time density, while the
111
+ vertical axis represents price.
112
+
113
+ In this study, the concept is adapted to microsecond unit data.
114
+ Instead of 30-minute TPO letters, each histogram bar represents the
115
+ number of data updates (or interpolated price levels, in the
116
+ gap-filled approach) observed at that price. The construction is
117
+ based exclusively on raw bid/ask unit data---not on TOHLC candles or
118
+ volume bars---ensuring that no information is lost to
119
+ aggregation~\cite{ane2000,engle2000}.
120
+
121
+ \subsection{Paper Outline}
122
+
123
+ Section~\ref{sec:data} describes the data acquisition pipeline and
124
+ the dataset used. Section~\ref{sec:raw} details the raw unit
125
+ approach. Section~\ref{sec:filled} introduces the gap-filled
126
+ (path-weighted) approach, including a detailed explanation of why
127
+ path-weighting is used. Section~\ref{sec:comparison} provides a
128
+ comprehensive comparison of the two approaches.
129
+ Section~\ref{sec:conclusion} concludes.
130
+
131
+
132
+ % ════════════════════════════════════════════════════════
133
+ \section{Data Acquisition}
134
+ \label{sec:data}
135
+
136
+ \subsection{Trading Environment}
137
+
138
+ The unit data used in this study was collected from a standard cent
139
+ live trading account on the Exness broker, accessed through MetaTrader~5.
140
+ MT5 is a multi-asset trading platform developed by MetaQuotes Software
141
+ Corp.\ that is widely used for forex and CFD
142
+ trading~\cite{mt5docs,metaquotes2024}. Its Python integration exposes
143
+ the function \texttt{copy\_ticks\_range()}, which returns every data
144
+ update within a specified time window as a structured NumPy
145
+ array~\cite{numpy2020}. Each data record contains the following
146
+ fields: a Unix timestamp in seconds, a millisecond-precision timestamp
147
+ providing sub-second resolution, the best bid price, the best ask
148
+ price, and additional metadata including flags indicating which fields
149
+ changed on that particular update.
150
+
151
+ Although the exposed timestamp has millisecond granularity, the MT5
152
+ documentation describes the system as operating at microsecond
153
+ internal resolution~\cite{mt5docs}; the millisecond field is what is
154
+ exposed through the Python API.
155
+
156
+ \subsection{Dataset Summary}
157
+
158
+ The symbol is \texttt{XAUUSDc}. The time range covers the full UTC
159
+ day of February~12, 2026, from 00:00:00 to 23:59:59. The flag used
160
+ retrieves all data updates regardless of whether the bid, ask, or last
161
+ price changed.
162
+
163
+ The query returned exactly \textbf{393,252~data points}. The first data point was
164
+ recorded at \textbf{2026-02-12 00:00:00.149~UTC} and the last data point at
165
+ \textbf{2026-02-12 23:59:57.820~UTC}. The bid price ranged from a
166
+ low of \textbf{\$4,878.380} to a high of \textbf{\$5,083.750}, a span
167
+ of \textbf{\$205.370} (20,537~units). The ask price ranged from
168
+ \textbf{\$4,878.620} to \textbf{\$5,083.990}, a span of
169
+ \textbf{\$205.370} (20,537~units).
170
+
171
+ \subsection{Unit Size}
172
+
173
+ The unit size for \texttt{XAUUSDc} is \textbf{\$0.010} (0.01~unit,
174
+ where 0.01~unit = \$0.01 XAU price change).
175
+ This value is determined by the broker's symbol specification and is
176
+ not configurable by the user. The lowest price resolution of XAU is
177
+ three decimal places: a change of 0.001 corresponds to a
178
+ \$0.001 price movement. Throughout this paper, $\delta = 0.010$
179
+ denotes the unit size, and the bin width used for histogram
180
+ construction equals 0.01~unit ($w = \delta = 0.010$).
181
+
182
+
183
+ % ════════════════════════════════════════════════════════
184
+ \section{Approach~1: Raw Unit Y-Distribution}
185
+ \label{sec:raw}
186
+
187
+ \subsection{Methodology}
188
+
189
+ The raw unit approach constructs a market profile histogram directly
190
+ from the 393,252 observed unit prices without any interpolation or
191
+ modification. The procedure begins by extracting the bid and ask
192
+ columns as separate arrays from the dataset. Histogram bin edges are
193
+ computed starting from
194
+ $\lfloor p_{\min}/\delta \rfloor \cdot \delta - \delta$ up to
195
+ $\lceil p_{\max}/\delta \rceil \cdot \delta + \delta$, spaced by
196
+ exactly $\delta = 0.010$. This ensures that every observed price
197
+ falls cleanly within a bin whose width is exactly 0.01~unit. Bin edges
198
+ are rounded to avoid floating-point precision
199
+ artefacts~\cite{goldberg1991}.
200
+
201
+ A standard frequency histogram is then computed---the count of data points
202
+ whose price falls within each bin---separately for bid and ask. The
203
+ histogram is plotted horizontally, with price on the vertical axis and
204
+ count on the horizontal axis, creating the conventional market-profile
205
+ appearance where the thickest region corresponds to the price level
206
+ that received the most data updates.
207
+
208
+ \subsection{Feature Engineering}
209
+
210
+ The feature engineering pipeline for the raw approach consists of the
211
+ following stages. First, the raw unit data from MT5 (a structured
212
+ array) is converted into a tabular format. The millisecond-precision
213
+ timestamp column is transformed into a UTC-aware datetime
214
+ representation. Next, the datetime values are converted to
215
+ floating-point date numbers suitable for high-performance
216
+ plotting~\cite{matplotlib2007}. This pre-conversion is performed once
217
+ before plotting because passing raw datetime objects to the plotting
218
+ library triggers an internal per-element conversion that is extremely
219
+ slow for arrays of 393,252 elements---the pre-conversion reduces
220
+ plotting time from several minutes to under one minute for the full
221
+ dataset.
222
+
223
+ The histogram bin edges are constructed using a range function with
224
+ step size equal to $\delta$ and then rounded. For the observed data
225
+ range of \$4,878.380 to \$5,083.990, this produces 20,563 bin edges
226
+ defining 20,562 bins, each exactly \$0.010 wide (0.01~unit).
227
+
228
+ \subsection{Output}
229
+
230
+ The output is a $2 \times 2$ subplot figure. The top row displays the
231
+ bid data: a horizontal histogram on the left (blue) and a time-series
232
+ line chart on the right (blue). The bottom row displays the ask data
233
+ in the same layout using red. The two rows share their respective
234
+ Y-axes so that price levels align horizontally between the histogram
235
+ and the line chart.
236
+
237
+ \begin{figure*}[t]
238
+ \centering
239
+ \includegraphics[width=\textwidth]{raw_ticks_4panel.png}
240
+ \caption{Raw unit Y-distribution histograms (left column) and
241
+ time-series line charts (right column) for bid (top, blue) and ask
242
+ (bottom, red) prices of \texttt{XAUUSDc} on February~12, 2026.
243
+ The dataset contains 393,252 data points. Bin size = 0.01~unit (\$0.010).
244
+ The histogram X-axis shows the count of data points observed at each
245
+ price level.}
246
+ \label{fig:raw_4panel}
247
+ \end{figure*}
248
+
249
+ \subsection{Interpretation}
250
+
251
+ In the raw histogram (Figure~\ref{fig:raw_4panel}), the count at each
252
+ price level reflects how many times the market's best bid or best ask
253
+ was updated to that exact price. Levels where the market
254
+ consolidated---spending extended time with many small quote
255
+ updates---accumulate high counts and form the thick horizontal bars in
256
+ the profile~\cite{dalton2007}.
257
+
258
+ However, when the market jumps from price $A$ to price $B$ in a single
259
+ update without quoting any intermediate level, those intermediate levels
260
+ receive zero counts in the histogram. The raw profile therefore
261
+ contains \emph{gaps}---entire price levels with no
262
+ representation---that correspond to fast directional moves. This is a
263
+ fundamental limitation: the profile faithfully records only what was
264
+ quoted, but it does not capture the price path traversed between
265
+ observations. This motivates the gap-filled approach presented in
266
+ Section~\ref{sec:filled}.
267
+
268
+
269
+ % ════════════════════════════════════════════════════════
270
+ \section{Approach~2: Gap-Filled (Path-Weighted) Y-Distribution}
271
+ \label{sec:filled}
272
+
273
+ \subsection{Motivation}
274
+
275
+ Consider a scenario where the bid price moves from \$5,060.000 to
276
+ \$5,060.100 in a single update. In the raw approach, only two price
277
+ levels---\$5,060.000 and \$5,060.100---register a count, while the
278
+ eight intermediate levels (\$5,060.010 through \$5,060.090) receive no
279
+ representation at all. Yet, under the assumption that price is a
280
+ continuous process sampled at discrete intervals, the price must have
281
+ traversed those eight levels to arrive at
282
+ \$5,060.100~\cite{cont2001,bacry2012}. The gap-filled approach
283
+ addresses this by inserting synthetic trail-datapoints at every
284
+ intermediate unit-level price between consecutive observations,
285
+ thereby constructing a profile that reflects the full path traversed
286
+ by the market rather than only the endpoints of each move.
287
+
288
+ \subsection{Why Path-Weighting?}
289
+ \label{sec:whypathweight}
290
+
291
+ The term \emph{path-weighted} refers to the fact that each price
292
+ level's histogram count is weighted by the number of times the price
293
+ path crossed that level, not merely the number of times it was
294
+ explicitly quoted. The rationale for this weighting rests on three
295
+ observations:
296
+
297
+ \begin{enumerate}[leftmargin=*]
298
+ \item \textbf{Continuity of the price process.} Financial prices
299
+ are fundamentally continuous stochastic processes sampled at
300
+ discrete intervals by the exchange or broker
301
+ feed~\cite{cont2001,bacry2012}. Between any two consecutive
302
+ observations at prices $p_A$ and $p_B$, the underlying price
303
+ process must have traversed every intermediate level. The raw
304
+ profile discards this traversal information; the path-weighted
305
+ profile recovers it.
306
+
307
+ \item \textbf{Elimination of empty bins.} In the raw profile,
308
+ fast directional moves produce stretches of price levels with zero
309
+ counts, creating discontinuities in the histogram that can mislead
310
+ visual interpretation. Path-weighting ensures that every price
311
+ level between $p_{\min}$ and $p_{\max}$ receives a non-zero count,
312
+ producing a continuous and visually coherent
313
+ profile~\cite{steidlmayer1986}.
314
+
315
+ \item \textbf{Traversal as a proxy for significance.} A price
316
+ level that is crossed repeatedly---even by fast-moving price
317
+ swings that do not dwell there---is a level that the market
318
+ revisits often. Such levels frequently correspond to support,
319
+ resistance, or areas of high liquidity~\cite{dalton2007,
320
+ steidlmayer1986}. Path-weighting captures this repeated-traversal
321
+ signal, which raw unit counting misses entirely.
322
+ \end{enumerate}
323
+
324
+ In summary, path-weighting transforms the market profile from a
325
+ histogram of \emph{quoting intensity} into a histogram of
326
+ \emph{traversal frequency}, which is a richer and more informative
327
+ representation of where the market has been.
328
+
329
+ \subsection{Algorithm}
330
+
331
+ The gap-filling algorithm operates on pairs of consecutive data points. For
332
+ each pair $(A, B)$ with prices $p_A$ and $p_B$ and timestamps $t_A$
333
+ and $t_B$ (represented as nanosecond integers for computational
334
+ efficiency), the algorithm first computes the signed unit difference
335
+ $\Delta n = \text{round}((p_B - p_A) / \delta)$. If
336
+ $|\Delta n| \le 1$, no interpolation is needed because the two prices
337
+ are adjacent or identical, and the pair is left unchanged. If
338
+ $|\Delta n| > 1$, the algorithm inserts $|\Delta n| - 1$ intermediate
339
+ rows. Each intermediate row $k$ (where $1 \le k < |\Delta n|$)
340
+ receives a price of
341
+ $p_A + k \cdot \text{sgn}(\Delta n) \cdot \delta$ and a timestamp of
342
+ $t_A + \frac{k}{|\Delta n|} \cdot (t_B - t_A)$. The timestamp
343
+ interpolation is linear, distributing the intermediate points evenly
344
+ across the time interval between data points $A$ and
345
+ $B$~\cite{dacorogna2001}.
346
+
347
+ The implementation is fully vectorised using array operations rather
348
+ than interpreted loops~\cite{numpy2020}. The key operations are
349
+ element repetition (to repeat each source index by the number of units
350
+ in its segment), cumulative summation (to compute segment start
351
+ positions), and element-wise arithmetic for price and timestamp
352
+ interpolation. This vectorised approach processes the entire
353
+ 393,252-point dataset in under 2~seconds on a consumer-grade machine.
354
+
355
+ The gap-filling is applied independently to the bid series and the ask
356
+ series because the bid and ask prices can move by different amounts on
357
+ the same data update. After gap-filling, the bid series expands from
358
+ 393,252 rows to exactly \textbf{4,614,400~rows} (an expansion factor
359
+ of $11.73\times$), and the ask series expands from 393,252 rows to
360
+ exactly \textbf{4,619,918~rows} (an expansion factor of
361
+ $11.75\times$).
362
+
363
+ \subsection{Feature Engineering}
364
+
365
+ The feature engineering pipeline for the gap-filled approach shares the
366
+ initial stages with the raw approach: data fetching, tabular
367
+ conversion, and datetime derivation are identical. The additional
368
+ stage is the gap-filling itself, which produces two new arrays of
369
+ expanded prices and their corresponding interpolated timestamps.
370
+
371
+ For plotting, the expanded nanosecond timestamps must be converted to
372
+ floating-point date numbers. Because the expanded arrays contain
373
+ approximately 4.6 million elements, calling a datetime conversion
374
+ function on individual objects would be prohibitively slow. Instead,
375
+ the conversion is performed arithmetically: the nanosecond integer is
376
+ divided by $10^9$ to get seconds, then by 86,400 to get fractional
377
+ days since the Unix epoch, and finally offset by the appropriate
378
+ constant to align with the plotting library's date
379
+ system~\cite{matplotlib2007}. This bypasses all object-level datetime
380
+ creation and processes the 4.6 million timestamps in a single
381
+ vectorised operation.
382
+
383
+ The histogram bins are constructed identically to the raw approach,
384
+ using 0.01-unit (\$0.010) bin widths. Because the gap-filled data has
385
+ the same price range as the raw data (\$4,878.380 to \$5,083.990),
386
+ the number of bins is also 20,562.
387
+
388
+ \subsection{Output}
389
+
390
+ The output figure has the identical $2 \times 2$ subplot layout as
391
+ Figure~\ref{fig:raw_4panel}.
392
+
393
+ \begin{figure*}[t]
394
+ \centering
395
+ \includegraphics[width=\textwidth]{filled_ticks_4panel.png}
396
+ \caption{Gap-filled (path-weighted) Y-distribution histograms
397
+ (left column) and time-series line charts (right column) for bid
398
+ (top, blue) and ask (bottom, red) prices of \texttt{XAUUSDc} on
399
+ February~12, 2026. The bid series contains 4,614,400 data points
400
+ and the ask series contains 4,619,918 data points after
401
+ gap-filling. Bin size = 0.01~unit (\$0.010). The histogram X-axis
402
+ shows the path-weighted count: the number of times each price
403
+ level was traversed between consecutive data points, including synthetic
404
+ intermediate points.}
405
+ \label{fig:filled_4panel}
406
+ \end{figure*}
407
+
408
+ \subsection{Interpretation}
409
+
410
+ The gap-filled histogram (Figure~\ref{fig:filled_4panel}) answers a
411
+ fundamentally different question than the raw histogram. Where the
412
+ raw profile asks ``how many times was price \emph{quoted} at this
413
+ level,'' the gap-filled profile asks ``how many times did the price
414
+ \emph{path} cross this level.'' The practical consequence is visible
415
+ in the histogram scale: the raw histogram peaks at counts near 120,
416
+ while the gap-filled histogram peaks at counts near 1,200 (consistent
417
+ with the $\approx 11.7\times$ average expansion factor).
418
+
419
+ Price regions that were traversed frequently---even if the market did
420
+ not dwell there long enough to generate many raw tick
421
+ updates---accumulate higher counts in the gap-filled profile. The
422
+ large sell-off visible around 16:00~UTC, where the bid price dropped
423
+ from the \$5,050.000 region to the \$4,878.000 region in a
424
+ concentrated burst of activity, produces substantial counts at every
425
+ intermediate price level in the gap-filled profile, whereas those same
426
+ levels appear sparse or empty in the raw profile because the market
427
+ jumped through them in large increments.
428
+
429
+
430
+ % ════════════════════════════════════════════════════════
431
+ \section{Raw vs.\ Gap-Filled: Comprehensive Comparison}
432
+ \label{sec:comparison}
433
+
434
+ \subsection{What Each Approach Measures}
435
+
436
+ The raw approach counts only actual tick updates from the broker's
437
+ data feed. When a price level receives a high count, it means the
438
+ market's best bid or ask was actively updated to that level many
439
+ times. This is a direct measurement of \emph{quoting
440
+ intensity}~\cite{ohara1995}: how frequently market participants were
441
+ placing or modifying orders at that price.
442
+
443
+ The gap-filled approach counts every tick-level price between
444
+ consecutive updates, including synthetic intermediate points that were
445
+ never explicitly quoted. When a price level receives a high count in
446
+ the gap-filled profile, it means the price \emph{path} crossed that
447
+ level many times---either through actual quoting or through
448
+ interpolation during price jumps. This is a measurement of
449
+ \emph{traversal frequency}.
450
+
451
+ \subsection{Detailed Comparison}
452
+
453
+ Table~\ref{tab:comparison} presents a comprehensive side-by-side
454
+ comparison of the two approaches across all relevant variables.
455
+
456
+ \begin{table*}[t]
457
+ \centering
458
+ \caption{Comprehensive comparison of raw tick vs.\ gap-filled
459
+ (path-weighted) market profile construction for \texttt{XAUUSDc} on
460
+ February~12, 2026.}
461
+ \label{tab:comparison}
462
+ \small
463
+ \begin{tabular}{@{}p{3.8cm}p{5.8cm}p{5.8cm}@{}}
464
+ \toprule
465
+ \textbf{Variable} & \textbf{Raw Tick Profile} & \textbf{Gap-Filled (Path-Weighted) Profile} \\
466
+ \midrule
467
+ Data source &
468
+ Microsecond bid/ask ticks from MT5 &
469
+ Same raw ticks, plus synthetic trail-datapoints \\
470
+ \midrule
471
+ Bid data points &
472
+ 393,252 &
473
+ 4,614,400 ($11.73\times$ expansion) \\
474
+ \midrule
475
+ Ask data points &
476
+ 393,252 &
477
+ 4,619,918 ($11.75\times$ expansion) \\
478
+ \midrule
479
+ Price range &
480
+ \$4,878.380 -- \$5,083.990 &
481
+ \$4,878.380 -- \$5,083.990 (identical) \\
482
+ \midrule
483
+ Bin width &
484
+ $\delta = \$0.010$ (1 tick) &
485
+ $\delta = \$0.010$ (1 tick, identical) \\
486
+ \midrule
487
+ Number of bins &
488
+ 20,562 &
489
+ 20,562 (identical) \\
490
+ \midrule
491
+ Avg.\ count per bin (bid) &
492
+ $393{,}252 / 20{,}537 \approx 19.15$ &
493
+ $4{,}614{,}400 / 20{,}537 \approx 224.7$ \\
494
+ \midrule
495
+ Peak histogram count &
496
+ $\sim$120 &
497
+ $\sim$1,200 \\
498
+ \midrule
499
+ Empty bins in profile &
500
+ Many (fast moves leave gaps) &
501
+ None (all intermediate levels filled) \\
502
+ \midrule
503
+ Profile continuity &
504
+ Discontinuous; sparse in trending regions &
505
+ Continuous; no gaps across entire price range \\
506
+ \midrule
507
+ What is measured &
508
+ Quoting intensity (how often each level was quoted) &
509
+ Traversal frequency (how often price path crossed each level) \\
510
+ \midrule
511
+ Consolidation zones &
512
+ High counts---dense, well-represented &
513
+ Similar to raw (few gaps to fill when moves are small) \\
514
+ \midrule
515
+ Fast directional moves &
516
+ Sparse or empty---underrepresented &
517
+ Well-represented with interpolated traversals \\
518
+ \midrule
519
+ Support/resistance detection &
520
+ Based on quoting density only &
521
+ Enhanced: repeated traversals indicate revisited levels \\
522
+ \midrule
523
+ Interpolation method &
524
+ None &
525
+ Linear timestamp interpolation, tick-step price fill \\
526
+ \midrule
527
+ Computational cost &
528
+ Minimal (direct histogram of raw data) &
529
+ Higher ($\sim$11.7$\times$ more data to process) \\
530
+ \bottomrule
531
+ \end{tabular}
532
+ \end{table*}
533
+
534
+ \subsection{Superiority of the Gap-Filled Approach}
535
+
536
+ The gap-filled approach produces a fundamentally more representative
537
+ market profile than the raw tick approach. Its advantages are
538
+ threefold:
539
+
540
+ \begin{enumerate}[leftmargin=*]
541
+ \item \textbf{Complete price coverage.} The gap-filled profile
542
+ assigns a non-zero count to every price level within the day's
543
+ range, eliminating the misleading empty bins that appear in the
544
+ raw profile during fast moves. This provides a structurally
545
+ complete picture of where the market traded.
546
+
547
+ \item \textbf{Traversal information.} By counting path crossings
548
+ rather than only explicit quotes, the gap-filled profile captures
549
+ information about how frequently the market revisited each price
550
+ level---information that is entirely absent from the raw profile.
551
+ This traversal signal is directly relevant to identifying dynamic
552
+ support and resistance~\cite{dalton2007}.
553
+
554
+ \item \textbf{Robustness to feed granularity.} Different brokers
555
+ and feed providers update tick data at different rates. A slower
556
+ feed produces larger jumps between consecutive ticks, which
557
+ creates more gaps in the raw profile. The gap-filled approach is
558
+ robust to this variation because it reconstructs the intermediate
559
+ path regardless of the feed's update frequency.
560
+ \end{enumerate}
561
+
562
+ The primary trade-off is computational cost: the gap-filling process
563
+ multiplies the dataset by a factor of approximately $11.7\times$ in
564
+ this study, which proportionally increases the time required for
565
+ histogram computation and rendering compared to a typical raw-data
566
+ market profile. For very long time horizons or very volatile
567
+ instruments, this expansion factor could be significantly larger.
568
+
569
+ \subsection{Interaction with Bin Size}
570
+
571
+ At the 1-tick bin width ($w = 0.010$) used throughout this study,
572
+ the difference between the raw and gap-filled profiles is maximal
573
+ because gaps in the raw profile (empty bins where no tick was
574
+ observed) are filled in by the gap-filling process. As the bin width
575
+ increases, the practical difference between the two approaches
576
+ diminishes because larger bins tend to capture at least some ticks
577
+ even in the raw profile, and the synthetic intermediate points are
578
+ absorbed into the same bins as the observed ticks. At sufficiently
579
+ large bin widths, the raw and gap-filled histograms become nearly
580
+ indistinguishable~\cite{scott1979}.
581
+
582
+
583
+ % ════════════════════════════════════════════════════════
584
+ \section{Conclusion}
585
+ \label{sec:conclusion}
586
+
587
+ This paper presented two approaches to constructing market profiles
588
+ from 393,252 microsecond-resolution bid/ask tick updates of
589
+ \texttt{XAUUSDc} on February~12, 2026, collected from a standard cent
590
+ live trading account on the Exness broker via MetaTrader~5. The raw
591
+ approach counted only observed tick levels, producing a profile that
592
+ reflects quoting intensity. The gap-filled (path-weighted) approach
593
+ interpolated every intermediate price level between consecutive ticks,
594
+ expanding the dataset to 4,614,400 bid rows and 4,619,918 ask rows,
595
+ and producing a profile that reflects path traversal frequency.
596
+
597
+ The gap-filled approach yields a more complete and informative market
598
+ profile by eliminating empty bins, capturing traversal information,
599
+ and providing robustness to variations in feed update frequency. The
600
+ primary cost of this approach is computational: the gap-filling
601
+ process multiplies the dataset size by a factor of approximately
602
+ $11.7\times$, which may result in slower calculation times compared to
603
+ typical market profile construction from raw data.
604
+
605
+ %% ============================================================================
606
+ %% AUTHOR INFORMATION
607
+ %% ============================================================================
608
+ \newpage
609
+ \vspace{2em}
610
+ \section*{Author Information}
611
+ \label{sec:author_info}
612
+
613
+ \begin{center}
614
+ \textbf{Rembrant Oyangoren Albeos}~\href{https://orcid.org/0009-0006-8743-4419}{\includegraphics[height=10pt]{ORCID_icon.png}}
615
+ \end{center}
616
+
617
+ \noindent\textbf{ORCID:} \url{https://orcid.org/0009-0006-8743-4419}
618
+
619
+ \noindent\textbf{Email:} algorembrant@gmail.com
620
+
621
+ \noindent\textbf{Affiliation:} Developer \& Researcher at ConQ
622
+
623
+ \noindent\textbf{Organization:} Continual Quasars~\includegraphics[height=7pt]{ContinualQuasars_icon.png}
624
+
625
+ \noindent\textbf{Organization GitHub:} \url{https://github.com/ContinualQuasars}
626
+
627
+ \noindent\textbf{This Version:} Febuary 14, 2026
628
+
629
+ \noindent\textbf{GitHub:} \url{https://github.com/ContinualQuasars/mBA-Profile}
630
+
631
+
632
+ % ════════════════════════════════════════════════════════
633
+ \newpage
634
+ \vspace{20}
635
+ \begin{thebibliography}{99}
636
+
637
+ \bibitem{steidlmayer1986}
638
+ J.~P. Steidlmayer and K.~Koy,
639
+ \textit{Markets and Market Logic},
640
+ Porcupine Press, 1986.
641
+
642
+ \bibitem{dalton2007}
643
+ J.~Dalton, E.~Jones, and R.~Dalton,
644
+ \textit{Mind Over Markets: Power Trading with Market Generated
645
+ Information}, Wiley, 2007.
646
+
647
+ \bibitem{mt5docs}
648
+ MetaQuotes Software Corp.,
649
+ ``MetaTrader~5 Python Integration,''
650
+ \url{https://www.mql5.com/en/docs/python_metatrader5}, 2024.
651
+
652
+ \bibitem{metaquotes2024}
653
+ MetaQuotes Software Corp.,
654
+ ``MetaTrader~5 Trading Platform,''
655
+ \url{https://www.metatrader5.com}, 2024.
656
+
657
+ \bibitem{ohara1995}
658
+ M.~O'Hara,
659
+ \textit{Market Microstructure Theory},
660
+ Blackwell Publishers, 1995.
661
+
662
+ \bibitem{hasbrouck2007}
663
+ J.~Hasbrouck,
664
+ \textit{Empirical Market Microstructure: The Institutions, Economics,
665
+ and Econometrics of Securities Trading},
666
+ Oxford University Press, 2007.
667
+
668
+ \bibitem{cont2001}
669
+ R.~Cont,
670
+ ``Empirical properties of asset returns: Stylized facts and
671
+ statistical issues,''
672
+ \textit{Quantitative Finance}, vol.~1, no.~2, pp.~223--236, 2001.
673
+
674
+ \bibitem{bacry2012}
675
+ E.~Bacry, M.~Mastromatteo, and J.-F. Muzy,
676
+ ``Hawkes processes in finance,''
677
+ \textit{Market Microstructure and Liquidity}, vol.~1, no.~1, 2015.
678
+
679
+ \bibitem{dacorogna2001}
680
+ M.~M. Dacorogna, R.~Gen\c{c}ay, U.~A. M\"{u}ller, R.~B. Olsen, and
681
+ O.~V. Pictet,
682
+ \textit{An Introduction to High-Frequency Finance},
683
+ Academic Press, 2001.
684
+
685
+ \bibitem{engle2000}
686
+ R.~F. Engle and J.~R. Russell,
687
+ ``Autoregressive conditional duration: A new model for irregularly
688
+ spaced transaction data,''
689
+ \textit{Econometrica}, vol.~66, no.~5, pp.~1127--1162, 1998.
690
+
691
+ \bibitem{ane2000}
692
+ T.~An\'{e} and H.~Geman,
693
+ ``Order flow, transaction clock, and normality of asset returns,''
694
+ \textit{The Journal of Finance}, vol.~55, no.~5, pp.~2259--2284,
695
+ 2000.
696
+
697
+ \bibitem{goldberg1991}
698
+ D.~Goldberg,
699
+ ``What every computer scientist should know about floating-point
700
+ arithmetic,''
701
+ \textit{ACM Computing Surveys}, vol.~23, no.~1, pp.~5--48, 1991.
702
+
703
+ \bibitem{numpy2020}
704
+ C.~R. Harris \textit{et al.},
705
+ ``Array programming with NumPy,''
706
+ \textit{Nature}, vol.~585, pp.~357--362, 2020.
707
+
708
+ \bibitem{matplotlib2007}
709
+ J.~D. Hunter,
710
+ ``Matplotlib: A 2D graphics environment,''
711
+ \textit{Computing in Science \& Engineering}, vol.~9, no.~3,
712
+ pp.~90--95, 2007.
713
+
714
+ \bibitem{cmegroup2024}
715
+ CME Group,
716
+ ``Gold Futures Contract Specifications,''
717
+ \url{https://www.cmegroup.com/markets/metals/precious/gold.contractSpecs.html},
718
+ 2024.
719
+
720
+ \bibitem{scott1979}
721
+ D.~W. Scott,
722
+ ``On optimal and data-based histograms,''
723
+ \textit{Biometrika}, vol.~66, no.~3, pp.~605--610, 1979.
724
+
725
+ \end{thebibliography}
726
+
727
+
728
+
729
+ \end{document}
730
+
output/filled_ask_XAUUSDc_20260212.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4bb8d664f7c5a62ba89c59050d50b5af3a23ac9fc97055ae7bcd62cd18aa007b
3
+ size 206497481
output/filled_bid_XAUUSDc_20260212.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:39ea0b63d98e6b8127956f9546e210088e2a132c6ae497b1b9c16bade40e2f3a
3
+ size 206250499
output/raw_ticks_XAUUSDc_20260212.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2d65fb9a10b9979529a28551b3be2ddf2bb30487f00c3d1267bdf214563b957a
3
+ size 24292358
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MetaTrader5
2
+ pandas
3
+ numpy
4
+ matplotlib
5
+ datetime
6
+
7
+ PyQt6
8
+ pyqtgraph
9
+ MetaTrader5
10
+ pandas
11
+ numpy
scripts/debug_mt5.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sys
3
+ import os
4
+ import MetaTrader5 as mt5
5
+ from datetime import datetime
6
+
7
+ # Add project root to sys.path
8
+ current_dir = os.path.dirname(os.path.abspath(__file__))
9
+ project_root = os.path.dirname(current_dir)
10
+ sys.path.insert(0, project_root)
11
+
12
+ try:
13
+ import src.config as config
14
+ except ImportError:
15
+ print("Error: Could not import src.config")
16
+ config = None
17
+
18
+ def test_connection():
19
+ print("=== MT5 Connection Debugger ===")
20
+ print(f"Python: {sys.version}")
21
+ print(f"MT5 Package Version: {mt5.__version__}")
22
+ print(f"MT5 Package Author: {mt5.__author__}")
23
+
24
+ path = getattr(config, 'MT5_PATH', '')
25
+ login = getattr(config, 'MT5_LOGIN', 0)
26
+ server = getattr(config, 'MT5_SERVER', '')
27
+ password = getattr(config, 'MT5_PASSWORD', '')
28
+
29
+ print(f"\nConfigured Path: '{path}'")
30
+ print(f"Configured Login: {login}")
31
+ print(f"Configured Server: '{server}'")
32
+ print(f"Configured Password: {'******' if password else 'Not Set'}")
33
+
34
+ print("\nAttempting Initialization...")
35
+
36
+ if path:
37
+ if not mt5.initialize(path=path):
38
+ print(f"FAILED: mt5.initialize(path='{path}')")
39
+ print(f"Error Code: {mt5.last_error()}")
40
+ return
41
+ else:
42
+ if not mt5.initialize():
43
+ print("FAILED: mt5.initialize()")
44
+ print(f"Error Code: {mt5.last_error()}")
45
+ print("Tip: If you have multiple MT5 terminals or it's installed in a custom location, set MT5_PATH in src/config.py")
46
+ return
47
+
48
+ print("SUCCESS: MT5 Initialized.")
49
+
50
+ # Check Terminal Info
51
+ terminal_info = mt5.terminal_info()
52
+ if terminal_info:
53
+ print("\nTerminal Info:")
54
+ print(f" Path: {terminal_info.path}")
55
+ print(f" Name: {terminal_info.name}")
56
+ print(f" Company: {terminal_info.company}")
57
+ print(f" Connected: {terminal_info.connected}")
58
+ else:
59
+ print("WARNING: Could not get terminal info.")
60
+
61
+ # Check Account Info
62
+ account_info = mt5.account_info()
63
+ if account_info:
64
+ print("\nAccount Info (Current):")
65
+ print(f" Login: {account_info.login}")
66
+ print(f" Server: {account_info.server}")
67
+ print(f" Variable Margin: {account_info.margin_so_mode}")
68
+ else:
69
+ print("\nWARNING: No account currently logged in or accessible.")
70
+
71
+ # Attempt Login if provided
72
+ if login and password and server:
73
+ print(f"\nAttempting Login to {login} on {server}...")
74
+ authorized = mt5.login(login=login, password=password, server=server)
75
+ if authorized:
76
+ print("SUCCESS: Authorized.")
77
+ account_info = mt5.account_info()
78
+ print(f" Balance: {account_info.balance}")
79
+ print(f" Equity: {account_info.equity}")
80
+ else:
81
+ print(f"FAILED: Login failed. Error: {mt5.last_error()}")
82
+
83
+ mt5.shutdown()
84
+ print("\nDisconnected.")
85
+
86
+ if __name__ == "__main__":
87
+ test_connection()
src/config.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import time
3
+ import pytz
4
+
5
+ # ──────────────────────────────────────────────
6
+ # Global Configuration
7
+ # ──────────────────────────────────────────────
8
+
9
+ # MT5 Connection
10
+ MT5_PATH = r"" # Path to terminal64.exe, e.g., r"C:\Program Files\MetaTrader 5\terminal64.exe"
11
+ MT5_LOGIN = 0 # Account number (int)
12
+ MT5_PASSWORD = "" # Account password
13
+ MT5_SERVER = "" # Broker server name
14
+
15
+ DEFAULT_SYMBOL = "XAUUSD"
16
+ TIMEFRAME = 1 # 1 minute for basic candles if needed, but we focus on ticks
17
+
18
+ # Market Profile Settings
19
+ # Session defined as 22:00 UTC (previous day) to 00:00 UTC (current day) as per request?
20
+ # Actually user said: "after the feature-engineered micro marketprofile is established at 22 UTC to 0 UTC, then make sure there is a developing VAH VAL and POC line."
21
+ # This implies the "profile building" phase is 22:00 -> 00:00.
22
+ # And "developing" lines start from 00:00 onwards?
23
+ # We will define the PROFILE_START_TIME and PROFILE_END_TIME.
24
+
25
+ TIMEZONE_UTC = pytz.utc
26
+ PROFILE_START_TIME = time(22, 0, 0) # 22:00 UTC
27
+ PROFILE_END_TIME = time(0, 0, 0) # 00:00 UTC (next day effectively)
28
+
29
+ # The user might mean a 2-hour window [22, 00) to build the initial profile?
30
+ # Or maybe the "day" starts at 22:00 UTC?
31
+ # "focus on ... gap-filled ... approach ... established at 22 UTC to 0 UTC"
32
+ # Let's assume we use data from 22:00 (prev day) to 00:00 (current day) to ESTABLISH the levels.
33
+ # And then we CONTINUE updating/developing them? Or we use those fixed levels?
34
+ # "make sure there is a developing VAH VAL and POC line." -> implies they continue to evolve?
35
+ # Or maybe they start plotting from 00:00 based on the 22-00 data?
36
+ # I will implement it such that the profile *accumulates* starting from a specialized time.
37
+
38
+ # Visualization Colors
39
+ COLOR_BID = '#0000FF' # Blue
40
+ COLOR_ASK = '#FF0000' # Red
41
+ COLOR_VAH = '#00FF00' # Green
42
+ COLOR_VAL = '#FF00FF' # Magenta
43
+ COLOR_POC = '#FFFF00' # Yellow
44
+ COLOR_BACKGROUND = '#000000'
45
+ COLOR_TEXT = '#FFFFFF'
46
+
47
+ # Technical
48
+ UNIT_SIZE = 0.01 # For XAUUSD, $0.01
src/core/data_worker.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from datetime import datetime, timedelta, timezone
3
+ import pandas as pd
4
+ import numpy as np
5
+ from PyQt6.QtCore import QThread, pyqtSignal, QObject
6
+
7
+ from src.core.mt5_interface import MT5Interface
8
+ from src.core.market_profile import MarketProfile
9
+
10
+ class DataWorker(QThread):
11
+ # Signals
12
+ data_signal = pyqtSignal(object, object) # ticks_df, profile_counts
13
+ levels_signal = pyqtSignal(object, object, object, object) # times, vah, val, poc (can be arrays or scalars)
14
+ status_signal = pyqtSignal(str)
15
+ finished_signal = pyqtSignal()
16
+
17
+ def __init__(self, symbol, date_obj, multiplier=1.0):
18
+ super().__init__()
19
+ self.symbol = symbol
20
+ self.date_obj = date_obj
21
+ self.multiplier = multiplier
22
+ self.running = True
23
+ self.mt5_interface = MT5Interface()
24
+ self.market_profile = MarketProfile(multiplier=self.multiplier)
25
+
26
+ def run(self):
27
+ self.status_signal.emit(f"Connecting to MT5... (Multiplier: {self.multiplier}x)")
28
+ if not self.mt5_interface.initialize():
29
+ self.status_signal.emit("Failed to connect to MT5.")
30
+ self.finished_signal.emit()
31
+ return
32
+
33
+ # Calculate session times
34
+ # Target Date (00:00 UTC of the selected day)
35
+ target_date_utc = datetime(self.date_obj.year, self.date_obj.month, self.date_obj.day, tzinfo=timezone.utc)
36
+
37
+ # Establishment Start: 22:00 UTC previous day
38
+ start_establishment = target_date_utc - timedelta(days=1) + timedelta(hours=22)
39
+ # Establishment End (Developing Start): 00:00 UTC target day
40
+ end_establishment = target_date_utc
41
+ # Session End: 00:00 UTC next day (24h later)
42
+ end_session = target_date_utc + timedelta(days=1)
43
+
44
+ # Current time
45
+ now_utc = datetime.now(timezone.utc)
46
+
47
+ # Determine Fetch Range
48
+ is_historical = end_session < now_utc
49
+ fetch_end = end_session if is_historical else now_utc
50
+
51
+ self.status_signal.emit(f"Fetch Range: {start_establishment} to {fetch_end} ...")
52
+
53
+ # 1. Fetch History
54
+ ticks_df = self.mt5_interface.get_ticks(self.symbol, start_establishment, fetch_end)
55
+
56
+ if not ticks_df.empty:
57
+ # Split Data
58
+ mask_est = (ticks_df['datetime'] >= start_establishment) & (ticks_df['datetime'] < end_establishment)
59
+ df_est = ticks_df.loc[mask_est]
60
+
61
+ mask_dev = (ticks_df['datetime'] >= end_establishment)
62
+ df_dev = ticks_df.loc[mask_dev]
63
+
64
+ self.status_signal.emit(f"Data: {len(ticks_df)} total. Est: {len(df_est)}, Dev: {len(df_dev)}")
65
+
66
+ # 2. Process Establishment Phase
67
+ if not df_est.empty:
68
+ self.market_profile.update(df_est)
69
+ self.status_signal.emit(f"Profile Established. Ticks: {self.market_profile.total_ticks}")
70
+ else:
71
+ self.status_signal.emit("Warning: No Establishment Data (22:00-00:00). Starting empty.")
72
+
73
+ # 3. Process Developing Phase (History Replay)
74
+ dev_times = []
75
+ dev_vah = []
76
+ dev_val = []
77
+ dev_poc = []
78
+
79
+ if not df_dev.empty:
80
+ # Resample to 1 minute to calculate trajectory
81
+ df_dev_indexed = df_dev.set_index('datetime')
82
+ grouped = df_dev_indexed.resample('1min')
83
+
84
+ count_steps = 0
85
+ for time_idx, group in grouped:
86
+ if group.empty:
87
+ continue
88
+
89
+ # Update profile
90
+ group_reset = group.reset_index()
91
+ self.market_profile.update(group_reset)
92
+
93
+ # Calculate levels
94
+ v, l, p = self.market_profile.get_vah_val_poc()
95
+ if v is not None:
96
+ # Use timestamp
97
+ ts_float = time_idx.timestamp()
98
+ dev_times.append(ts_float)
99
+ dev_vah.append(v)
100
+ dev_val.append(l)
101
+ dev_poc.append(p)
102
+ count_steps += 1
103
+
104
+ self.status_signal.emit(f"Calculated {count_steps} developing points.")
105
+
106
+ # Emit History Data: Ticks
107
+ # If extremely large, maybe downsample? But for now send all.
108
+ print(f"DEBUG: Worker Emitting Ticks: {len(ticks_df)}")
109
+ self.data_signal.emit(ticks_df, self.market_profile.counts)
110
+
111
+ # Emit Levels
112
+ if dev_times:
113
+ print(f"DEBUG: Worker Emitting Levels: {len(dev_times)} pts. Times: {dev_times[0]} -> {dev_times[-1]}")
114
+ self.levels_signal.emit(
115
+ np.array(dev_times),
116
+ np.array(dev_vah),
117
+ np.array(dev_val),
118
+ np.array(dev_poc)
119
+ )
120
+ else:
121
+ print("DEBUG: No developing levels calculated to emit.")
122
+ self.status_signal.emit("No developing levels calculated (insufficient dev data?).")
123
+
124
+ else:
125
+ self.status_signal.emit("No ticks returned from MT5.")
126
+
127
+ # 4. Live Streaming (Only if not historical)
128
+ if not is_historical:
129
+ self.status_signal.emit("Live streaming active...")
130
+
131
+ last_time = now_utc
132
+ if not ticks_df.empty:
133
+ last_time = ticks_df['datetime'].iloc[-1].to_pydatetime()
134
+
135
+ while self.running:
136
+ time.sleep(1.0)
137
+
138
+ cur_time = datetime.now(timezone.utc)
139
+ new_ticks = self.mt5_interface.get_ticks(self.symbol, last_time, cur_time + timedelta(seconds=1))
140
+
141
+ if not new_ticks.empty:
142
+ new_ticks = new_ticks[new_ticks['datetime'] > last_time]
143
+
144
+ if not new_ticks.empty:
145
+ self.market_profile.update(new_ticks)
146
+ last_time = new_ticks['datetime'].iloc[-1].to_pydatetime()
147
+
148
+ # Emit Tick Data
149
+ # print(f"DEBUG: Live Tick Update: {len(new_ticks)}")
150
+ self.data_signal.emit(new_ticks, self.market_profile.counts)
151
+
152
+ # Emit Level Update
153
+ v, l, p = self.market_profile.get_vah_val_poc()
154
+ if v is not None:
155
+ ts_now = cur_time.timestamp()
156
+ # print(f"DEBUG: Live Level Update: POC {p}")
157
+ self.levels_signal.emit([ts_now], [v], [l], [p])
158
+ else:
159
+ self.status_signal.emit("Historical view loaded. Live stream inactive.")
160
+
161
+
162
+ def stop(self):
163
+ self.running = False
164
+ self.mt5_interface.shutdown()
165
+ self.wait()
src/core/market_profile.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+ from datetime import datetime, time
4
+
5
+ class MarketProfile:
6
+ def __init__(self, multiplier=2.0):
7
+ self.multiplier = multiplier
8
+ self.counts = {} # price -> count (time/tick opportunity)
9
+ self.total_ticks = 0
10
+ self.min_price = float('inf')
11
+ self.max_price = float('-inf')
12
+
13
+ def reset(self):
14
+ self.counts = {}
15
+ self.total_ticks = 0
16
+ self.min_price = float('inf')
17
+ self.max_price = float('-inf')
18
+
19
+ def fill_gaps(self, prices: np.ndarray, timestamps_ns: np.ndarray, step_sizes: np.ndarray):
20
+ """
21
+ Vectorised gap-fill with dynamic step sizes.
22
+ step_sizes: array of shape (N,) corresponding to each price point.
23
+ We use step_sizes[:-1] for the gaps starting at prices[:-1].
24
+ Returns: (filled_prices, filled_timestamps_ns)
25
+ """
26
+ if len(prices) < 2:
27
+ return prices, timestamps_ns
28
+
29
+ # Step sizes for the intervals (from point i -> i+1)
30
+ # If scalar, broadcast. If array, slice.
31
+ if np.isscalar(step_sizes):
32
+ # Broadcast to shape (N-1,)
33
+ steps_interval = np.full(len(prices)-1, step_sizes, dtype=np.float64)
34
+ else:
35
+ # Assume step_sizes corresponds to prices. The step for gap i->i+1 is step_sizes[i].
36
+ steps_interval = step_sizes[:-1]
37
+
38
+ # Avoid division by zero or extremely small steps
39
+ steps_interval = np.where(steps_interval < 0.000001, 0.01, steps_interval)
40
+
41
+ diff = np.diff(prices)
42
+ # Number of units (steps) to fill for each gap
43
+ diff_units = np.round(diff / steps_interval).astype(np.int64)
44
+ counts = np.abs(diff_units)
45
+
46
+ # Last point gets a count of 1 (itself)
47
+ counts = np.append(counts, 1)
48
+
49
+ total = int(np.sum(counts))
50
+ if total == 0:
51
+ return prices, timestamps_ns
52
+
53
+ indices = np.repeat(np.arange(len(prices)), counts)
54
+
55
+ # Offset within each segment (0, 1, 2...)
56
+ cum = np.cumsum(counts)
57
+ starts = np.empty_like(cum)
58
+ starts[0] = 0
59
+ starts[1:] = cum[:-1]
60
+ offsets = np.arange(total) - np.repeat(starts, counts)
61
+
62
+ # Direction per segment (+1 or -1)
63
+ directions = np.zeros(len(prices), dtype=np.float64)
64
+ directions[:-1] = np.sign(diff_units)
65
+
66
+ # Time step per segment
67
+ # We need to interpolate time as well
68
+ dt = np.zeros(len(prices), dtype=np.float64)
69
+ dt[:-1] = np.diff(timestamps_ns).astype(np.float64)
70
+
71
+ # Avoid division by zero in time steps if counts is 0 (shouldn't happen with counts > 0 check, but be safe)
72
+ div_counts = np.where(counts > 0, counts, 1)
73
+ time_steps = dt / div_counts
74
+
75
+ # Expand step sizes and time steps
76
+ if np.isscalar(step_sizes):
77
+ expanded_steps = np.full(len(indices), step_sizes, dtype=np.float64)
78
+ else:
79
+ expanded_steps = step_sizes[indices]
80
+
81
+ expanded_time_steps = time_steps[indices]
82
+
83
+ # Calculate filled prices and times
84
+ filled_prices = prices[indices] + offsets * directions[indices] * expanded_steps
85
+ filled_ts = timestamps_ns[indices].astype(np.float64) + offsets * expanded_time_steps
86
+
87
+ return np.round(filled_prices, 2), filled_ts.astype(np.int64)
88
+
89
+ def update(self, ticks_df: pd.DataFrame):
90
+ """
91
+ Updates the profile with new ticks.
92
+ ticks_df must have 'bid', 'ask', 'datetime'.
93
+ """
94
+ if ticks_df.empty:
95
+ return
96
+
97
+ timestamps_ns = ticks_df['datetime'].values.astype('datetime64[ns]').astype(np.int64)
98
+ bids = ticks_df['bid'].values.astype(np.float64)
99
+
100
+ # Calculate dynamic step sizes based on Spread
101
+ # Spread = Ask - Bid
102
+ # Step = Spread * Multiplier
103
+
104
+ # Ensure 'ask' exists
105
+ if 'ask' in ticks_df.columns:
106
+ asks = ticks_df['ask'].values.astype(np.float64)
107
+ spreads = asks - bids
108
+ # Ensure non-negative/non-zero spread fallback
109
+ spreads = np.maximum(spreads, 0.00001)
110
+ step_sizes = spreads * self.multiplier
111
+
112
+ # Update Bid
113
+ self.add_data(bids, timestamps_ns, step_sizes)
114
+ # Update Ask
115
+ self.add_data(asks, timestamps_ns, step_sizes)
116
+
117
+ else:
118
+ # Fallback if no ask column
119
+ step_sizes = np.full(len(bids), 0.01 * self.multiplier)
120
+ self.add_data(bids, timestamps_ns, step_sizes)
121
+
122
+ def add_data(self, prices: np.ndarray, timestamps_ns: np.ndarray, step_sizes: np.ndarray):
123
+ """
124
+ Gap-fills the data and updates the histogram counts.
125
+ """
126
+ filled_prices, filled_ts = self.fill_gaps(prices, timestamps_ns, step_sizes)
127
+
128
+ # Update histogram
129
+ unique, counts = np.unique(filled_prices, return_counts=True)
130
+
131
+ for p, c in zip(unique, counts):
132
+ p = round(float(p), 2)
133
+ self.counts[p] = self.counts.get(p, 0) + c
134
+ self.total_ticks += c
135
+ if p < self.min_price: self.min_price = p
136
+ if p > self.max_price: self.max_price = p
137
+
138
+ def get_vah_val_poc(self):
139
+ """
140
+ Calculates Value Area High (VAH), Value Area Low (VAL), and Point of Control (POC).
141
+ Standard definition: 70% of volume around POC.
142
+ """
143
+ if not self.counts:
144
+ return None, None, None
145
+
146
+ # Convert to sorted list of (price, count)
147
+ sorted_prices = sorted(self.counts.keys())
148
+ counts_list = [self.counts[p] for p in sorted_prices]
149
+
150
+ counts_array = np.array(counts_list, dtype=np.int64)
151
+ prices_array = np.array(sorted_prices, dtype=np.float64)
152
+
153
+ # POC
154
+ poc_idx = np.argmax(counts_array)
155
+ poc_price = prices_array[poc_idx]
156
+
157
+ # Value Area (70%)
158
+ total_count = np.sum(counts_array)
159
+ target_count = total_count * 0.70
160
+
161
+ current_count = counts_array[poc_idx]
162
+ left_idx = poc_idx
163
+ right_idx = poc_idx
164
+
165
+ # Greedily expand
166
+ while current_count < target_count:
167
+ # Try to pick best side
168
+ can_go_left = left_idx > 0
169
+ can_go_right = right_idx < len(counts_array) - 1
170
+
171
+ if not can_go_left and not can_go_right:
172
+ break
173
+
174
+ count_left = counts_array[left_idx - 1] if can_go_left else -1
175
+ count_right = counts_array[right_idx + 1] if can_go_right else -1
176
+
177
+ if count_left > count_right:
178
+ current_count += count_left
179
+ left_idx -= 1
180
+ elif count_right > count_left:
181
+ current_count += count_right
182
+ right_idx += 1
183
+ else:
184
+ # Equal counts, expand both if possible
185
+ if can_go_left:
186
+ current_count += count_left
187
+ left_idx -= 1
188
+ if can_go_right:
189
+ current_count += count_right
190
+ right_idx += 1
191
+
192
+ val_price = prices_array[left_idx]
193
+ vah_price = prices_array[right_idx]
194
+
195
+ return vah_price, val_price, poc_price
src/core/mt5_interface.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import MetaTrader5 as mt5
2
+ import pandas as pd
3
+ from datetime import datetime, timedelta
4
+ import pytz
5
+ import time
6
+ import src.config as config
7
+
8
+ class MT5Interface:
9
+ def __init__(self):
10
+ self.connected = False
11
+
12
+ def initialize(self):
13
+ """Initializes the connection to MetaTrader 5."""
14
+ path = getattr(config, 'MT5_PATH', '')
15
+ login = getattr(config, 'MT5_LOGIN', 0)
16
+ password = getattr(config, 'MT5_PASSWORD', '')
17
+ server = getattr(config, 'MT5_SERVER', '')
18
+
19
+ if path:
20
+ if not mt5.initialize(path=path):
21
+ print(f"MT5 initialize(path={path}) failed, error code = {mt5.last_error()}")
22
+ self.connected = False
23
+ return False
24
+ else:
25
+ if not mt5.initialize():
26
+ print(f"MT5 initialize() failed, error code = {mt5.last_error()}")
27
+ self.connected = False
28
+ return False
29
+
30
+ # Attempt login if credentials provided
31
+ if login and password and server:
32
+ authorized = mt5.login(login=login, password=password, server=server)
33
+ if not authorized:
34
+ print(f"MT5 login failed, error code = {mt5.last_error()}")
35
+ mt5.shutdown()
36
+ self.connected = False
37
+ return False
38
+
39
+ print("MT5 Initialized successfully.")
40
+ self.connected = True
41
+ return True
42
+
43
+ def shutdown(self):
44
+ """Shuts down the connection."""
45
+ mt5.shutdown()
46
+ self.connected = False
47
+
48
+ def get_ticks(self, symbol, start_time_utc: datetime, end_time_utc: datetime):
49
+ """
50
+ Fetches ticks for a given symbol and time range.
51
+ Returns a DataFrame with 'time_msc', 'bid', 'ask', 'flags', 'volume'.
52
+ """
53
+ if not self.connected:
54
+ if not self.initialize():
55
+ return pd.DataFrame()
56
+
57
+ # Ensure timestamps are timezone-aware (UTC)
58
+ if start_time_utc.tzinfo is None:
59
+ start_time_utc = start_time_utc.replace(tzinfo=pytz.utc)
60
+ if end_time_utc.tzinfo is None:
61
+ end_time_utc = end_time_utc.replace(tzinfo=pytz.utc)
62
+
63
+ ticks = mt5.copy_ticks_range(symbol, start_time_utc, end_time_utc, mt5.COPY_TICKS_ALL)
64
+
65
+ if ticks is None or len(ticks) == 0:
66
+ print(f"No ticks found for {symbol} between {start_time_utc} and {end_time_utc}")
67
+ return pd.DataFrame()
68
+
69
+ df = pd.DataFrame(ticks)
70
+ # Convert time_msc to datetime
71
+ df['datetime'] = pd.to_datetime(df['time_msc'], unit='ms', utc=True)
72
+ return df
73
+
74
+ def get_last_tick(self, symbol):
75
+ """Fetches the latest tick for the symbol."""
76
+ if not self.connected:
77
+ return None
78
+
79
+ tick = mt5.symbol_info_tick(symbol)
80
+ return tick
81
+
82
+ def get_symbol_info(self, symbol):
83
+ """Returns symbol specification."""
84
+ info = mt5.symbol_info(symbol)
85
+ return info
src/main.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+
4
+ # Add project root to sys.path to ensure imports work
5
+ # We want the parent of 'src' (project root) in sys.path so 'import src.xxx' works
6
+ current_dir = os.path.dirname(os.path.abspath(__file__))
7
+ project_root = os.path.dirname(current_dir)
8
+ sys.path.append(project_root)
9
+
10
+ from PyQt6.QtWidgets import QApplication
11
+ from src.ui.main_window import MainWindow
12
+
13
+ def main():
14
+ app = QApplication(sys.argv)
15
+
16
+ # Optional: Set a dark theme/palette here
17
+
18
+ window = MainWindow()
19
+ window.show()
20
+
21
+ sys.exit(app.exec())
22
+
23
+ if __name__ == "__main__":
24
+ main()
src/ui/chart_widget.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pyqtgraph as pg
2
+ from PyQt6.QtWidgets import QWidget, QVBoxLayout
3
+ from PyQt6.QtCore import pyqtSlot
4
+ import numpy as np
5
+ import pandas as pd
6
+ from datetime import datetime
7
+ import pytz
8
+
9
+ class ChartWidget(QWidget):
10
+ def __init__(self, parent=None):
11
+ super().__init__(parent)
12
+ self.layout = QVBoxLayout()
13
+ self.setLayout(self.layout)
14
+
15
+ # Initialize PyQtGraph layout
16
+ self.glw = pg.GraphicsLayoutWidget()
17
+ self.layout.addWidget(self.glw)
18
+
19
+ # Col 0: Profile (Count vs Price)
20
+ self.profile_plot = self.glw.addPlot(row=0, col=0)
21
+ self.profile_plot.setMaximumWidth(200)
22
+ self.profile_plot.hideAxis('bottom')
23
+ self.profile_plot.showAxis('top')
24
+ self.profile_plot.setLabel('top', 'Volume')
25
+ self.profile_plot.setClipToView(True)
26
+ # self.profile_plot.setDownsampling(auto=True, mode='peak')
27
+
28
+ # Col 1: Price (Time vs Price)
29
+ self.price_plot = self.glw.addPlot(row=0, col=1)
30
+ self.price_plot.setLabel('bottom', 'Time')
31
+ self.price_plot.setLabel('right', 'Price')
32
+ self.price_plot.showAxis('right')
33
+ self.price_plot.hideAxis('left')
34
+ self.price_plot.showAxis('right')
35
+ self.price_plot.hideAxis('left')
36
+ self.price_plot.setClipToView(False) # Disable for debugging
37
+ # self.price_plot.setDownsampling(auto=True, mode='peak') # Disable for debugging
38
+
39
+ # Link Y-axes
40
+ # self.profile_plot.setYLink(self.price_plot) # temporarily unlink to rule out profile plot issues
41
+
42
+ # Initialize Chart Items
43
+ self.bid_curve = self.price_plot.plot(pen=pg.mkPen('b', width=1), name="Bid")
44
+ self.ask_curve = self.price_plot.plot(pen=pg.mkPen('r', width=1), name="Ask")
45
+
46
+ # Profile Histogram Item
47
+ self.profile_bars = pg.BarGraphItem(x0=0, y=0, width=0, height=0.01, brush='c')
48
+ self.profile_plot.addItem(self.profile_bars)
49
+
50
+ # Developing Lines (PlotCurveItems)
51
+ self.curve_vah = self.price_plot.plot(pen=pg.mkPen('g', width=2), name="VAH")
52
+ self.curve_val = self.price_plot.plot(pen=pg.mkPen('m', width=2), name="VAL")
53
+ self.curve_poc = self.price_plot.plot(pen=pg.mkPen('y', width=2), name="POC")
54
+
55
+ # Date axis formatter
56
+ self.date_axis = self.price_plot.getAxis('bottom')
57
+ # self.date_axis.setTickSpacing(3600, 1800) # Grid every hour - Caused MemoryError
58
+ self.price_plot.showGrid(x=True, y=True, alpha=0.3)
59
+
60
+ # Data storage
61
+ self.times = np.array([])
62
+ self.bids = np.array([])
63
+ self.asks = np.array([])
64
+
65
+ # Storage for developing levels
66
+ self.level_times = np.array([])
67
+ self.level_vah = np.array([])
68
+ self.level_val = np.array([])
69
+ self.level_poc = np.array([])
70
+
71
+ def clear(self):
72
+ self.times = np.array([])
73
+ self.bids = np.array([])
74
+ self.asks = np.array([])
75
+ self.level_times = np.array([])
76
+ self.level_vah = np.array([])
77
+ self.level_val = np.array([])
78
+ self.level_poc = np.array([])
79
+
80
+ self.bid_curve.setData([], [])
81
+ self.ask_curve.setData([], [])
82
+ self.profile_bars.setOpts(x0=[], y=[], width=[], height=[])
83
+ self.curve_vah.setData([], [])
84
+ self.curve_val.setData([], [])
85
+ self.curve_poc.setData([], [])
86
+
87
+ def update_ticks(self, df):
88
+ """
89
+ Updates the tick chart by appending new data.
90
+ df: DataFrame with 'datetime' (ns timestamp) and 'bid', 'ask'.
91
+ """
92
+ if df.empty:
93
+ return
94
+
95
+ # Convert timestamps for pyqtgraph (seconds since epoch)
96
+ # df['datetime'] is numpy datetime64[ns]
97
+ new_times = df['datetime'].values.astype(np.float64) / 1e9
98
+ new_bids = df['bid'].values
99
+ new_asks = df['ask'].values if 'ask' in df.columns else np.zeros_like(new_bids)
100
+
101
+ if len(self.times) == 0:
102
+ self.times = new_times
103
+ self.bids = new_bids
104
+ self.asks = new_asks
105
+ else:
106
+ self.times = np.concatenate((self.times, new_times))
107
+ self.bids = np.concatenate((self.bids, new_bids))
108
+ self.asks = np.concatenate((self.asks, new_asks))
109
+
110
+ # Debug Log
111
+ if len(self.times) > 0:
112
+ t_min, t_max = self.times[0], self.times[-1]
113
+ b_min, b_max = np.min(self.bids), np.max(self.bids)
114
+ print(f"DEBUG: Chart Ticks: {len(self.times)} pts.")
115
+ print(f"DEBUG: Time Range: {t_min:.1f} -> {t_max:.1f} ({datetime.fromtimestamp(t_min)} -> {datetime.fromtimestamp(t_max)})")
116
+ print(f"DEBUG: Price Range: {b_min:.4f} -> {b_max:.4f}")
117
+
118
+ # Update curves
119
+ self.bid_curve.setData(self.times, self.bids)
120
+ if len(self.asks) > 0:
121
+ self.ask_curve.setData(self.times, self.asks)
122
+
123
+ # Force range on first large update
124
+ if len(self.times) > 0 and len(self.times) == len(new_times):
125
+ self.price_plot.setXRange(self.times[0], self.times[-1], padding=0.02)
126
+ self.price_plot.setYRange(np.min(self.bids), np.max(self.bids), padding=0.02)
127
+
128
+ def update_profile(self, counts_dict, unit_size=0.01):
129
+ """
130
+ Updates the side profile histogram.
131
+ counts: dict {price: count}
132
+ """
133
+ if not counts_dict:
134
+ return
135
+
136
+ prices = np.array(list(counts_dict.keys()))
137
+ counts = np.array(list(counts_dict.values()))
138
+
139
+ # Horizontal bars: x0=0, y=prices, width=counts, height=unit_size
140
+ self.profile_bars.setOpts(x0=np.zeros(len(prices)), y=prices, width=counts, height=unit_size, brush=(0, 255, 255, 100))
141
+
142
+ def update_levels(self, new_times, new_vah, new_val, new_poc):
143
+ """
144
+ Updates the developing VAH/VAL/POC lines.
145
+ Expects arrays or scalars.
146
+ """
147
+ try:
148
+ # Ensure inputs are 1D arrays
149
+ nt = np.atleast_1d(np.array(new_times, dtype=np.float64))
150
+ nv = np.atleast_1d(np.array(new_vah, dtype=np.float64))
151
+ nl = np.atleast_1d(np.array(new_val, dtype=np.float64))
152
+ yp = np.atleast_1d(np.array(new_poc, dtype=np.float64))
153
+
154
+ if len(nt) == 0:
155
+ # print("Chart Update Levels: Empty new_times")
156
+ return
157
+
158
+ # Append logic
159
+ if len(self.level_times) == 0:
160
+ self.level_times = nt
161
+ self.level_vah = nv
162
+ self.level_val = nl
163
+ self.level_poc = yp
164
+ else:
165
+ self.level_times = np.concatenate((self.level_times, nt))
166
+ self.level_vah = np.concatenate((self.level_vah, nv))
167
+ self.level_val = np.concatenate((self.level_val, nl))
168
+ self.level_poc = np.concatenate((self.level_poc, yp))
169
+
170
+ if len(nt) > 1:
171
+ print(f"DEBUG: Chart Levels Loaded: {len(nt)} pts. POC Range: {self.level_poc[0]:.2f} -> {self.level_poc[-1]:.2f}")
172
+
173
+ # Update plots
174
+ self.curve_vah.setData(self.level_times, self.level_vah)
175
+ self.curve_val.setData(self.level_times, self.level_val)
176
+ self.curve_poc.setData(self.level_times, self.level_poc)
177
+ except Exception as e:
178
+ print(f"Error updating levels: {e}")
src/ui/control_panel.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt6.QtWidgets import (
2
+ QWidget, QVBoxLayout, QLabel, QLineEdit, QDateEdit,
3
+ QPushButton, QGroupBox, QFormLayout, QDoubleSpinBox
4
+ )
5
+ from PyQt6.QtCore import QDate, pyqtSignal
6
+
7
+ class ControlPanel(QWidget):
8
+ # Signals to notify Main Window
9
+ # Adjusted to include multiplier
10
+ start_signal = pyqtSignal(str, object, float) # symbol, date, multiplier
11
+ stop_signal = pyqtSignal()
12
+
13
+ def __init__(self, parent=None):
14
+ super().__init__(parent)
15
+ self.init_ui()
16
+
17
+ def init_ui(self):
18
+ layout = QVBoxLayout()
19
+
20
+ # Group: Settings
21
+ group = QGroupBox("Settings")
22
+ form = QFormLayout()
23
+
24
+ self.symbol_input = QLineEdit("XAUUSD")
25
+
26
+ self.date_input = QDateEdit()
27
+ self.date_input.setDate(QDate.currentDate())
28
+ self.date_input.setCalendarPopup(True)
29
+
30
+ self.multiplier_input = QDoubleSpinBox()
31
+ self.multiplier_input.setRange(0.1, 100.0)
32
+ self.multiplier_input.setDecimals(2)
33
+ self.multiplier_input.setValue(2.0)
34
+ self.multiplier_input.setSingleStep(0.5)
35
+
36
+ form.addRow("Symbol:", self.symbol_input)
37
+ form.addRow("Date:", self.date_input)
38
+ form.addRow("Spread Multiplier (x):", self.multiplier_input)
39
+
40
+ group.setLayout(form)
41
+ layout.addWidget(group)
42
+
43
+ # Buttons
44
+ self.btn_start = QPushButton("Start Stream")
45
+ self.btn_start.clicked.connect(self.on_start)
46
+ self.btn_start.setStyleSheet("background-color: green; color: white; font-weight: bold;")
47
+
48
+ self.btn_stop = QPushButton("Stop Stream")
49
+ self.btn_stop.clicked.connect(self.on_stop)
50
+ self.btn_stop.setStyleSheet("background-color: red; color: white; font-weight: bold;")
51
+ self.btn_stop.setEnabled(False)
52
+
53
+ layout.addWidget(self.btn_start)
54
+ layout.addWidget(self.btn_stop)
55
+
56
+ layout.addStretch()
57
+ self.setLayout(layout)
58
+
59
+ def on_start(self):
60
+ symbol = self.symbol_input.text()
61
+ date = self.date_input.date().toPyDate()
62
+ multiplier = self.multiplier_input.value()
63
+ self.start_signal.emit(symbol, date, multiplier)
64
+
65
+ self.btn_start.setEnabled(False)
66
+ self.btn_stop.setEnabled(True)
67
+ self.symbol_input.setEnabled(False)
68
+ self.date_input.setEnabled(False)
69
+ self.multiplier_input.setEnabled(False)
70
+
71
+ def on_stop(self):
72
+ self.stop_signal.emit()
73
+
74
+ self.btn_start.setEnabled(True)
75
+ self.btn_stop.setEnabled(False)
76
+ self.symbol_input.setEnabled(True)
77
+ self.date_input.setEnabled(True)
78
+ self.multiplier_input.setEnabled(True)
src/ui/main_window.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt6.QtWidgets import (
2
+ QMainWindow, QDockWidget, QStatusBar, QMessageBox, QWidget
3
+ )
4
+ from PyQt6.QtCore import Qt
5
+
6
+ from src.ui.control_panel import ControlPanel
7
+ from src.ui.chart_widget import ChartWidget
8
+ from src.core.data_worker import DataWorker
9
+
10
+ class MainWindow(QMainWindow):
11
+ def __init__(self):
12
+ super().__init__()
13
+ self.setWindowTitle("Python Trading Terminal (MT5 + Gap-Filled Profile)")
14
+ self.resize(1200, 800)
15
+
16
+ self.init_ui()
17
+ self.worker = None
18
+
19
+ def init_ui(self):
20
+ # Status Bar
21
+ self.status_bar = QStatusBar()
22
+ self.setStatusBar(self.status_bar)
23
+
24
+ # Dock: Control Panel
25
+ self.dock_controls = QDockWidget("Controls", self)
26
+ self.control_panel = ControlPanel()
27
+ self.dock_controls.setWidget(self.control_panel)
28
+ self.dock_controls.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea)
29
+ self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.dock_controls)
30
+
31
+ # Central: Chart
32
+ self.chart_widget = ChartWidget()
33
+ self.setCentralWidget(self.chart_widget)
34
+
35
+ # Connections
36
+ self.control_panel.start_signal.connect(self.start_worker)
37
+ self.control_panel.stop_signal.connect(self.stop_worker)
38
+
39
+ def start_worker(self, symbol, date, multiplier):
40
+ if self.worker is not None and self.worker.isRunning():
41
+ return
42
+
43
+ self.chart_widget.clear()
44
+ self.chart_widget.price_plot.setTitle(f"{symbol} - {date} (Multiplier: {multiplier}x)")
45
+
46
+ self.worker = DataWorker(symbol, date, multiplier=multiplier)
47
+ self.worker.status_signal.connect(self.status_bar.showMessage)
48
+ self.worker.data_signal.connect(self.handle_data)
49
+ self.worker.levels_signal.connect(self.handle_levels)
50
+ self.worker.finished.connect(self.on_worker_finished)
51
+
52
+ self.worker.start()
53
+
54
+ def stop_worker(self):
55
+ if self.worker:
56
+ self.worker.stop()
57
+ self.worker = None
58
+ self.status_bar.showMessage("Stream stopped.")
59
+
60
+ def handle_data(self, ticks_df, profile_counts):
61
+ # Update Chart with new ticks
62
+ # Note: ChartWidget.update_ticks expects a dataframe.
63
+ # If we just append, we might need to handle state inside chart widget better.
64
+ # But simpler: pass the full set or update logic.
65
+ # For performance, we should probably append.
66
+ # In current ChartWidget, update_ticks SETS data.
67
+ # DataWorker emits chunks (new_ticks) during loop, but FULL history at start.
68
+ # We need to distinguish or accumulate in ChartWidget?
69
+ # Actually, DataWorker emits distinct chunks in the loop.
70
+ # But ChartWidget `setData` replaces content.
71
+ # We need to accumulate data in ChartWidget or pass accumulated data from Worker.
72
+ # Passing Full DF every 500ms with millions of rows is bad.
73
+ # Better: ChartWidget accumulates.
74
+
75
+ # Actually, let's just make ChartWidget append.
76
+ # Or better: Worker sends everything? No.
77
+ # Let's Modify ChartWidget to accumulate.
78
+
79
+ # WAIT: My ChartWidget implementation `update_ticks` does: `self.bid_curve.setData(t_float, bids)`
80
+ # This REPLACES.
81
+ # I need to fix ChartWidget to handle incremental updates or large data better.
82
+ # For now, let's just pass the full accumulated history from Worker?
83
+ # Worker doesn't store accumulated history openly, it just emits `ticks_df` (chunk).
84
+ # We need a way to append.
85
+
86
+ # Let's fix ChartWidget in next step. For now assume it appends.
87
+ self.chart_widget.update_ticks(ticks_df)
88
+ self.chart_widget.update_profile(profile_counts) # Full profile is cheap (dict)
89
+
90
+ def handle_levels(self, times, vah, val, poc):
91
+ self.chart_widget.update_levels(times, vah, val, poc)
92
+
93
+ def on_worker_finished(self):
94
+ self.control_panel.on_stop()
95
+ self.worker = None
96
+ self.status_bar.showMessage("Ready.")
tests/test_interactive_chart.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import numpy as np
4
+ import pandas as pd
5
+ from PyQt6.QtWidgets import QApplication
6
+ from PyQt6.QtCore import QTimer
7
+ import pyqtgraph as pg
8
+
9
+ # Add project root to path
10
+ sys.path.append(os.getcwd())
11
+
12
+ from src.ui.chart_widget import ChartWidget
13
+
14
+ def verify_chart_render():
15
+ app = QApplication(sys.argv)
16
+ widget = ChartWidget()
17
+ widget.resize(800, 600)
18
+ widget.show()
19
+
20
+ print("Widget shown. Generating dummy data...")
21
+
22
+ # 1. Simulate Tick Data
23
+ now = pd.Timestamp.now()
24
+ dates = pd.date_range(start=now, periods=100, freq='1s')
25
+ bids = np.linspace(100, 105, 100) + np.random.normal(0, 0.1, 100)
26
+ asks = bids + 0.2
27
+
28
+ df = pd.DataFrame({
29
+ 'datetime': dates,
30
+ 'bid': bids,
31
+ 'ask': asks
32
+ })
33
+
34
+ # Update ticks
35
+ widget.update_ticks(df)
36
+ print("Ticks updated.")
37
+
38
+ # Check if curves have data
39
+ x, y = widget.bid_curve.getData()
40
+ if x is not None and len(x) == 100:
41
+ print("PASS: Bid curve has data.")
42
+ else:
43
+ print(f"FAIL: Bid curve data mismatch. Len: {len(x) if x is not None else 0}")
44
+
45
+ # 2. Simulate Levels
46
+ # timestamps for 10 min
47
+ level_times = [dates[i].timestamp() for i in range(0, 100, 10)]
48
+ level_vah = np.linspace(101, 104, 10)
49
+ level_val = np.linspace(99, 102, 10)
50
+ level_poc = np.linspace(100, 103, 10)
51
+
52
+ widget.update_levels(level_times, level_vah, level_val, level_poc)
53
+ print("Levels updated.")
54
+
55
+ x_vah, y_vah = widget.curve_vah.getData()
56
+ if x_vah is not None and len(x_vah) == 10:
57
+ print("PASS: VAH curve has data.")
58
+ else:
59
+ print(f"FAIL: VAH curve data mismatch. Len: {len(x_vah) if x_vah is not None else 0}")
60
+
61
+ # Set a timer to close the app automatically after a few seconds if running in automation
62
+ # QTimer.singleShot(2000, app.quit)
63
+
64
+ # For now, just quit immediately to verify logic
65
+ app.quit()
66
+ print("Test finished.")
67
+
68
+ if __name__ == "__main__":
69
+ verify_chart_render()
tests/test_logic.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import unittest
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ # Add src to path
8
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
9
+
10
+ from src.core.market_profile import MarketProfile
11
+
12
+ class TestMarketProfile(unittest.TestCase):
13
+ def setUp(self):
14
+ self.mp = MarketProfile(unit_size=1.0) # Simple unit size
15
+
16
+ def test_gap_fill(self):
17
+ prices = np.array([100.0, 105.0])
18
+ timestamps = np.array([0, 100], dtype=np.int64)
19
+
20
+ filled = self.mp.fill_gaps(prices, timestamps)
21
+
22
+ # Expected: 100, 101, 102, 103, 104, 105
23
+ expected = np.array([100.0, 101.0, 102.0, 103.0, 104.0, 105.0])
24
+
25
+ np.testing.assert_array_equal(filled, expected)
26
+
27
+ def test_profile_calculation(self):
28
+ # Create a skewed profile
29
+ # 100: 10 counts
30
+ # 101: 20 counts (POC)
31
+ # 102: 5 counts
32
+
33
+ # Construct dataframe
34
+ data = {
35
+ 'bid': np.concatenate([
36
+ np.full(10, 100.0),
37
+ np.full(20, 101.0),
38
+ np.full(5, 102.0)
39
+ ]),
40
+ 'datetime': np.zeros(35, dtype='datetime64[ns]') # Timestamps don't matter much for counts
41
+ }
42
+ df = pd.DataFrame(data)
43
+
44
+ # Since gap filling needs consecutive diffs, and here we have flat regions,
45
+ # gap filling on [100, 100] produces just [100, 100].
46
+ # But `fill_gaps` logic: diff=0 -> count=0 -> total=0?
47
+ # My implementation: counts = abs(diff) ... append 1 for last point.
48
+ # If diff=0, count=0. Total = 0 + 1 = 1.
49
+ # It handles flat lines correctly (just repeats the point).
50
+
51
+ self.mp.update(df)
52
+
53
+ vah, val, poc = self.mp.get_vah_val_poc()
54
+
55
+ self.assertEqual(poc, 101.0)
56
+
57
+ # Total vol = 35. 70% = 24.5.
58
+ # POC volume = 20.
59
+ # Neighbors: 100 (10), 102 (5).
60
+ # 100 is larger. So it should expand to 100.
61
+ # Current vol = 20 + 10 = 30 > 24.5. Stop.
62
+ # So Value Area = [100, 101].
63
+ # VAL = 100, VAH = 101.
64
+
65
+ self.assertEqual(val, 100.0)
66
+ self.assertEqual(vah, 101.0)
67
+
68
+ if __name__ == '__main__':
69
+ unittest.main()
tests/test_profile_logic.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import pandas as pd
4
+ import numpy as np
5
+
6
+ # Add project root to sys.path
7
+ current_dir = os.path.dirname(os.path.abspath(__file__))
8
+ project_root = os.path.dirname(current_dir)
9
+ sys.path.append(project_root)
10
+
11
+ from src.core.market_profile import MarketProfile
12
+
13
+ def test_market_profile_logic():
14
+ print("Testing Market Profile Logic (Bid + Ask + Timestamp Interpolation)...")
15
+
16
+ # 1. Setup Data
17
+ # Bid moves from 100.00 to 101.00 (Diff 1.0)
18
+ # Ask moves from 100.10 to 101.10 (Diff 1.0)
19
+ # Spread = 0.10
20
+ # Multiplier = 2.0 -> Bin Size = 0.20
21
+
22
+ # Expected Gaps for Bid: 1.0 / 0.20 = 5 steps.
23
+ # Bid points: 100.0, 100.2, 100.4, 100.6, 100.8, 101.0 -> 6 points.
24
+
25
+ # Expected Gaps for Ask: 1.0 / 0.20 = 5 steps.
26
+ # Ask points: 100.1, 100.3, 100.5, 100.7, 100.9, 101.1 -> 6 points.
27
+
28
+ # Total Ticks = 12.
29
+
30
+ multiplier = 2.0
31
+
32
+ data = {
33
+ 'datetime': [pd.Timestamp('2023-01-01 10:00:00'), pd.Timestamp('2023-01-01 10:00:01')],
34
+ 'bid': [100.00, 101.00],
35
+ 'ask': [100.10, 101.10]
36
+ }
37
+ df = pd.DataFrame(data)
38
+
39
+ # 2. Initialize Profile
40
+ mp = MarketProfile(multiplier=multiplier)
41
+
42
+ # 3. Update
43
+ mp.update(df)
44
+
45
+ # 4. Verify
46
+ print(f"Total Ticks (Count sum): {mp.total_ticks}")
47
+ expected_ticks = 12
48
+
49
+ if mp.total_ticks == expected_ticks:
50
+ print("SUCCESS: Total Ticks match expected (Bid + Ask filled).")
51
+ else:
52
+ print(f"FAILURE: Expected {expected_ticks}, got {mp.total_ticks}")
53
+
54
+ print(f"Counts: {mp.counts}")
55
+
56
+ # Check specific prices
57
+ expected_bids = [100.00, 100.20, 100.40, 100.60, 100.80, 101.00]
58
+ expected_asks = [100.10, 100.30, 100.50, 100.70, 100.90, 101.10]
59
+ all_expected = expected_bids + expected_asks
60
+
61
+ missing = []
62
+ for p in all_expected:
63
+ p_rounded = round(p, 2)
64
+ if p_rounded not in mp.counts:
65
+ missing.append(p_rounded)
66
+
67
+ if not missing:
68
+ print("SUCCESS: All expected Bid and Ask gap prices found.")
69
+ else:
70
+ print(f"FAILURE: Missing prices: {missing}")
71
+
72
+ if __name__ == "__main__":
73
+ test_market_profile_logic()
tests/verify_levels.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import sys
4
+ import os
5
+
6
+ # Add project root to path
7
+ sys.path.append(os.getcwd())
8
+
9
+ from src.core.market_profile import MarketProfile
10
+
11
+ def test_market_profile():
12
+ print("Testing MarketProfile...")
13
+ mp = MarketProfile(multiplier=2.0)
14
+
15
+ # Create dummy data
16
+ # Price oscillates between 100 and 110
17
+ dates = pd.date_range(start='2024-01-01 10:00', periods=100, freq='1min')
18
+ bids = np.linspace(100, 110, 50)
19
+ bids = np.concatenate((bids, np.linspace(110, 100, 50)))
20
+
21
+ df = pd.DataFrame({
22
+ 'datetime': dates,
23
+ 'bid': bids,
24
+ 'ask': bids + 0.1
25
+ })
26
+
27
+ # Update profile
28
+ mp.update(df)
29
+
30
+ # Check counts
31
+ print(f"Total ticks: {mp.total_ticks}")
32
+ print(f"Counts keys: {len(mp.counts)}")
33
+
34
+ # Check Levels
35
+ vah, val, poc = mp.get_vah_val_poc()
36
+ print(f"VAH: {vah}, VAL: {val}, POC: {poc}")
37
+
38
+ if vah is None or val is None or poc is None:
39
+ print("FAIL: Levels are None")
40
+ else:
41
+ print("PASS: Levels calculated")
42
+
43
+ if __name__ == "__main__":
44
+ test_market_profile()