| |
|
| | """
|
| | generate_profiles.py β mBA-GMP.v3 Dataframe & Chart Generator
|
| | ==============================================================
|
| | Produces CSV files and publication-quality PNG charts demonstrating the
|
| | Conventional Market Profile (CMP), Gap-filled Market Profile (GMP),
|
| | and Up/Down-Bin Footprint Profile using a 10-datapoint XAUUSD example.
|
| |
|
| | Outputs:
|
| | CSV: datapoints.csv, cmp_profile.csv, gmp_profile.csv,
|
| | updown_profile.csv
|
| | PNG: fig_price_scatter.png, fig_cmp_profile.png,
|
| | fig_gmp_profile.png, fig_cmp_vs_gmp.png,
|
| | fig_updown_footprint.png
|
| | """
|
| |
|
| | import math
|
| | import csv
|
| | import os
|
| |
|
| |
|
| | try:
|
| | import matplotlib
|
| | matplotlib.use("Agg")
|
| | import matplotlib.pyplot as plt
|
| | import matplotlib.ticker as ticker
|
| | HAS_MPL = True
|
| | except ImportError:
|
| | HAS_MPL = False
|
| | print("[WARN] matplotlib not found β CSV files will still be generated "
|
| | "but PNG charts will be skipped.")
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | DATAPOINTS = [
|
| | ("A", 1, 3000.914),
|
| | ("B", 2, 3003.837),
|
| | ("C", 3, 3002.432),
|
| | ("D", 4, 3009.892),
|
| | ("E", 5, 3007.698),
|
| | ("F", 6, 3009.176),
|
| | ("G", 7, 3003.381),
|
| | ("H", 8, 3004.283),
|
| | ("I", 9, 3003.512),
|
| | ("J", 10, 3003.012),
|
| | ]
|
| |
|
| | BIN_SIZE = 1
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def bin_index(price: float, beta: float = BIN_SIZE) -> int:
|
| | """Return the bin index for a given price: floor(price / Ξ²)."""
|
| | return int(math.floor(price / beta))
|
| |
|
| |
|
| | def bin_range(price: float, beta: float = BIN_SIZE):
|
| | """Return (price_from, price_until) for the bin containing *price*."""
|
| | b = bin_index(price, beta)
|
| | return b * beta, (b + 1) * beta
|
| |
|
| |
|
| | def make_bin_key(b: int, beta: float = BIN_SIZE):
|
| | """Return (bin_number_1based, price_from, price_until) for bin index *b*."""
|
| | return (b * beta, (b + 1) * beta)
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def build_cmp(datapoints, beta=BIN_SIZE):
|
| | """
|
| | Build CMP profile.
|
| | Returns dict: bin_index -> {"labels": [str], "count": int}
|
| | """
|
| | profile = {}
|
| | for label, _trade, price in datapoints:
|
| | b = bin_index(price, beta)
|
| | if b not in profile:
|
| | profile[b] = {"labels": [], "count": 0}
|
| | profile[b]["labels"].append(label)
|
| | profile[b]["count"] += 1
|
| | return profile
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def build_gmp(datapoints, beta=BIN_SIZE):
|
| | """
|
| | Build GMP profile (gap-filled).
|
| |
|
| | Convention (matches the dataframe approach):
|
| | 1. Every datapoint fills its OWN bin with its own label (same as CMP).
|
| | 2. For each consecutive pair (i, i+1), the intermediate bins BETWEEN
|
| | b(p_i) and b(p_{i+1}) β exclusive of both endpoints β are filled
|
| | with the SOURCE datapoint's label (datapoint i).
|
| |
|
| | Returns dict: bin_index -> {"labels": [str], "count": int}
|
| | """
|
| | profile = {}
|
| |
|
| | def add_to_bin(b, label):
|
| | if b not in profile:
|
| | profile[b] = {"labels": [], "count": 0}
|
| | profile[b]["labels"].append(label)
|
| | profile[b]["count"] += 1
|
| |
|
| |
|
| | for label, _trade, price in datapoints:
|
| | add_to_bin(bin_index(price, beta), label)
|
| |
|
| |
|
| | for idx in range(len(datapoints) - 1):
|
| | src_label, _, src_price = datapoints[idx]
|
| | _dst_label, _, dst_price = datapoints[idx + 1]
|
| |
|
| | b_from = bin_index(src_price, beta)
|
| | b_to = bin_index(dst_price, beta)
|
| |
|
| | if abs(b_to - b_from) <= 1:
|
| |
|
| | continue
|
| |
|
| | direction = 1 if b_to > b_from else -1
|
| |
|
| | b = b_from + direction
|
| | while b != b_to:
|
| | add_to_bin(b, src_label)
|
| | b += direction
|
| |
|
| | return profile
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def build_updown_profile(datapoints, beta=BIN_SIZE):
|
| | """
|
| | Build the Up/Down-Bin Footprint Profile.
|
| |
|
| | For each consecutive pair of datapoints, every bin on the gap-filled
|
| | path (excluding the source datapoint's own bin) is classified as an
|
| | up-bin or down-bin depending on the direction of the move.
|
| |
|
| | The first datapoint (no prior movement) receives 0 up / 0 down.
|
| |
|
| | Returns dict: bin_index -> {"labels": [str], "up": int, "down": int}
|
| | """
|
| |
|
| | groups = {}
|
| |
|
| | def add_label(b, label):
|
| | if b not in groups:
|
| | groups[b] = []
|
| | groups[b].append(label)
|
| |
|
| |
|
| | for label, _trade, price in datapoints:
|
| | add_label(bin_index(price, beta), label)
|
| |
|
| |
|
| | for idx in range(len(datapoints) - 1):
|
| | src_label, _, src_price = datapoints[idx]
|
| | _, _, dst_price = datapoints[idx + 1]
|
| | b_from = bin_index(src_price, beta)
|
| | b_to = bin_index(dst_price, beta)
|
| | if abs(b_to - b_from) <= 1:
|
| | continue
|
| | direction = 1 if b_to > b_from else -1
|
| | b = b_from + direction
|
| | while b != b_to:
|
| | add_label(b, src_label)
|
| | b += direction
|
| |
|
| |
|
| | up_counts = {}
|
| | down_counts = {}
|
| |
|
| | for idx in range(len(datapoints) - 1):
|
| | _, _, src_price = datapoints[idx]
|
| | _, _, dst_price = datapoints[idx + 1]
|
| |
|
| | b_from = bin_index(src_price, beta)
|
| | b_to = bin_index(dst_price, beta)
|
| |
|
| | if b_from == b_to:
|
| |
|
| | if dst_price > src_price:
|
| | up_counts[b_from] = up_counts.get(b_from, 0) + 1
|
| | elif dst_price < src_price:
|
| | down_counts[b_from] = down_counts.get(b_from, 0) + 1
|
| | continue
|
| |
|
| | is_up = b_to > b_from
|
| | direction = 1 if is_up else -1
|
| |
|
| |
|
| |
|
| | b = b_from + direction
|
| | while True:
|
| | if is_up:
|
| | up_counts[b] = up_counts.get(b, 0) + 1
|
| | else:
|
| | down_counts[b] = down_counts.get(b, 0) + 1
|
| | if b == b_to:
|
| | break
|
| | b += direction
|
| |
|
| |
|
| | all_bins = set(groups.keys()) | set(up_counts.keys()) | set(down_counts.keys())
|
| | profile = {}
|
| | for b in all_bins:
|
| | profile[b] = {
|
| | "labels": sorted(groups.get(b, [])),
|
| | "up": up_counts.get(b, 0),
|
| | "down": down_counts.get(b, 0),
|
| | }
|
| | return profile
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def write_datapoints_csv(datapoints, path="datapoints.csv"):
|
| | """Write the raw datapoints to CSV."""
|
| | with open(path, "w", newline="") as f:
|
| | w = csv.writer(f)
|
| | w.writerow(["datapoint", "x-axis trades (raw trades or time)", "y-axis Price"])
|
| | for label, trade, price in datapoints:
|
| | w.writerow([label, trade, f"{price:.3f}"])
|
| | print(f"[OK] {path}")
|
| |
|
| |
|
| | def write_profile_csv(profile, beta, path):
|
| | """Write a profile (CMP or GMP) to CSV, bins numbered 1..N from lowest."""
|
| | if not profile:
|
| | print(f"[WARN] Empty profile, skipping {path}")
|
| | return
|
| |
|
| | b_min = min(profile.keys())
|
| | b_max = max(profile.keys())
|
| |
|
| |
|
| | rows = []
|
| | bin_number = 1
|
| | for b in range(b_min, b_max + 1):
|
| | p_from = b * beta
|
| | p_until = (b + 1) * beta
|
| | info = profile.get(b, {"labels": [], "count": 0})
|
| | group = "".join(sorted(info["labels"]))
|
| | count = info["count"]
|
| | rows.append([bin_number, int(p_from), int(p_until), group, count])
|
| | bin_number += 1
|
| |
|
| | with open(path, "w", newline="") as f:
|
| | w = csv.writer(f)
|
| | w.writerow([
|
| | f"bin (with binsize = {beta} symbol's price unit)",
|
| | "price from", "price until", "datapoint group",
|
| | "number of profile's stacks"
|
| | ])
|
| | for row in rows:
|
| | w.writerow(row)
|
| | print(f"[OK] {path}")
|
| |
|
| |
|
| | def write_updown_profile_csv(updown_profile, gmp_groups, beta, path):
|
| | """Write the Up/Down-Bin Footprint Profile to CSV."""
|
| | if not updown_profile:
|
| | print(f"[WARN] Empty profile, skipping {path}")
|
| | return
|
| |
|
| | b_min = min(updown_profile.keys())
|
| | b_max = max(updown_profile.keys())
|
| |
|
| | rows = []
|
| | bin_number = 1
|
| | for b in range(b_min, b_max + 1):
|
| | p_from = b * beta
|
| | p_until = (b + 1) * beta
|
| | info = updown_profile.get(b, {"labels": [], "up": 0, "down": 0})
|
| | group = "".join(info["labels"])
|
| | up_val = info["up"]
|
| | down_val = info["down"]
|
| | delta_val = up_val - down_val
|
| | rows.append([bin_number, int(p_from), int(p_until), group,
|
| | down_val, up_val, delta_val])
|
| | bin_number += 1
|
| |
|
| | with open(path, "w", newline="") as f:
|
| | w = csv.writer(f)
|
| | w.writerow([
|
| | f"bin (with binsize = {beta} symbol's price unit)",
|
| | "price from", "price until", "datapoint group",
|
| | "down-bin profile's stacks", "up-bin profile's stacks",
|
| | "delta-bin profile's stacks"
|
| | ])
|
| | for row in rows:
|
| | w.writerow(row)
|
| | print(f"[OK] {path}")
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | CLR_BG = "#ffffff"
|
| | CLR_FG = "#1a1a1a"
|
| | CLR_GRID = "#d0d0d0"
|
| | CLR_ACCENT1 = "#1565c0"
|
| | CLR_ACCENT2 = "#e65100"
|
| | CLR_ACCENT3 = "#2e7d32"
|
| | CLR_MUTED = "#607d8b"
|
| | CLR_LABEL = "#333333"
|
| |
|
| | CHART_DPI = 300
|
| |
|
| |
|
| | def _apply_style(ax, title=""):
|
| | """Apply a consistent white/light theme to an axes object."""
|
| | ax.set_facecolor(CLR_BG)
|
| | ax.figure.set_facecolor(CLR_BG)
|
| | ax.tick_params(colors=CLR_FG, labelsize=8)
|
| | ax.xaxis.label.set_color(CLR_FG)
|
| | ax.yaxis.label.set_color(CLR_FG)
|
| | ax.title.set_color(CLR_FG)
|
| | for spine in ax.spines.values():
|
| | spine.set_color(CLR_GRID)
|
| | ax.grid(True, color=CLR_GRID, linewidth=0.5, alpha=0.4)
|
| | if title:
|
| | ax.set_title(title, fontsize=11, fontweight="bold", pad=10)
|
| |
|
| |
|
| | def chart_price_scatter(datapoints, path="fig_price_scatter.png", ax=None):
|
| | """Scatter + line plot of price vs trade index, labeled AβJ."""
|
| | labels = [d[0] for d in datapoints]
|
| | trades = [d[1] for d in datapoints]
|
| | prices = [d[2] for d in datapoints]
|
| |
|
| | standalone = ax is None
|
| | if standalone:
|
| | fig, ax = plt.subplots(figsize=(7, 4))
|
| | _apply_style(ax, "Price vs. Trade Index (Datapoints AβJ)")
|
| |
|
| | ax.plot(trades, prices, color=CLR_ACCENT1, linewidth=1.2, alpha=0.45,
|
| | zorder=1)
|
| | ax.scatter(trades, prices, color=CLR_ACCENT1, s=52, zorder=2,
|
| | edgecolors="white", linewidths=0.6)
|
| |
|
| | for lbl, x, y in zip(labels, trades, prices):
|
| | ax.annotate(lbl, (x, y), textcoords="offset points",
|
| | xytext=(0, 10), ha="center", fontsize=8,
|
| | fontweight="bold", color=CLR_LABEL)
|
| |
|
| | ax.set_xlabel("Trade Index (raw trades)", fontsize=9)
|
| | ax.set_ylabel("Price (USD)", fontsize=9)
|
| | ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.0f"))
|
| |
|
| | if standalone:
|
| | fig.tight_layout()
|
| | fig.savefig(path, dpi=CHART_DPI, bbox_inches="tight")
|
| | plt.close(fig)
|
| | print(f"[OK] {path}")
|
| |
|
| |
|
| | def _draw_profile(ax, profile, beta, title, bar_color):
|
| | """Draw a horizontal bar chart for a profile onto *ax*."""
|
| | b_min = min(profile.keys())
|
| | b_max = max(profile.keys())
|
| |
|
| | bin_labels = []
|
| | stacks = []
|
| | groups = []
|
| | for b in range(b_min, b_max + 1):
|
| | p_from = b * beta
|
| | p_until = (b + 1) * beta
|
| | bin_labels.append(f"{int(p_from)}β{int(p_until)}")
|
| | info = profile.get(b, {"labels": [], "count": 0})
|
| | stacks.append(info["count"])
|
| | groups.append("".join(sorted(info["labels"])))
|
| |
|
| | y_pos = range(len(bin_labels))
|
| | bars = ax.barh(y_pos, stacks, color=bar_color, edgecolor="white",
|
| | linewidth=0.5, height=0.7, alpha=0.85)
|
| |
|
| | ax.set_yticks(y_pos)
|
| | ax.set_yticklabels(bin_labels, fontsize=7)
|
| | ax.set_xlabel("Stacks", fontsize=9)
|
| | ax.set_ylabel("Price Bin (USD)", fontsize=9)
|
| |
|
| |
|
| | max_s = max(stacks) if stacks else 1
|
| | for i, (bar, grp) in enumerate(zip(bars, groups)):
|
| | if grp:
|
| | ax.text(bar.get_width() + 0.12, bar.get_y() + bar.get_height() / 2,
|
| | grp, va="center", ha="left", fontsize=7, color=CLR_LABEL,
|
| | fontweight="bold")
|
| |
|
| | ax.set_xlim(0, max_s + 2)
|
| | _apply_style(ax, title)
|
| |
|
| |
|
| | def chart_profile(profile, beta, path, title, bar_color):
|
| | """Standalone horizontal bar chart for a single profile (CMP or GMP)."""
|
| | if not profile:
|
| | return
|
| | fig, ax = plt.subplots(figsize=(6, 5))
|
| | _draw_profile(ax, profile, beta, title, bar_color)
|
| | fig.tight_layout()
|
| | fig.savefig(path, dpi=CHART_DPI, bbox_inches="tight")
|
| | plt.close(fig)
|
| | print(f"[OK] {path}")
|
| |
|
| |
|
| | def chart_cmp_vs_gmp(cmp_profile, gmp_profile, beta,
|
| | path="fig_cmp_vs_gmp.png"):
|
| | """Side-by-side comparison of CMP and GMP profiles (2-panel)."""
|
| | if not cmp_profile or not gmp_profile:
|
| | return
|
| |
|
| | fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 5), sharey=True)
|
| |
|
| | _draw_profile(ax1, cmp_profile, beta, "CMP Profile", CLR_ACCENT2)
|
| | _draw_profile(ax2, gmp_profile, beta, "GMP Profile", CLR_ACCENT3)
|
| | ax2.set_ylabel("")
|
| |
|
| | fig.suptitle("CMP vs. GMP β 10-Datapoint Example (Ξ² = 1)",
|
| | fontsize=13, fontweight="bold", color=CLR_FG, y=1.01)
|
| | fig.tight_layout()
|
| | fig.savefig(path, dpi=CHART_DPI, bbox_inches="tight")
|
| | plt.close(fig)
|
| | print(f"[OK] {path}")
|
| |
|
| |
|
| | def chart_combined_3panel(datapoints, cmp_profile, gmp_profile, beta,
|
| | path="fig_combined_3panel.png"):
|
| | """Three-panel chart: Datapoints | CMP with letters | GMP with letters."""
|
| | if not cmp_profile or not gmp_profile:
|
| | return
|
| |
|
| | fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16, 5.5),
|
| | gridspec_kw={"width_ratios": [1.1, 1, 1]})
|
| |
|
| |
|
| | labels = [d[0] for d in datapoints]
|
| | trades = [d[1] for d in datapoints]
|
| | prices = [d[2] for d in datapoints]
|
| |
|
| | _apply_style(ax1, "Datapoints (AβJ)")
|
| | ax1.plot(trades, prices, color=CLR_ACCENT1, linewidth=1.2, alpha=0.4,
|
| | zorder=1)
|
| | ax1.scatter(trades, prices, color=CLR_ACCENT1, s=52, zorder=2,
|
| | edgecolors="white", linewidths=0.6)
|
| | for lbl, x, y in zip(labels, trades, prices):
|
| | ax1.annotate(lbl, (x, y), textcoords="offset points",
|
| | xytext=(0, 10), ha="center", fontsize=9,
|
| | fontweight="bold", color=CLR_LABEL)
|
| | ax1.set_xlabel("Trade Index", fontsize=9)
|
| | ax1.set_ylabel("Price (USD)", fontsize=9)
|
| | ax1.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.0f"))
|
| |
|
| |
|
| | _draw_profile(ax2, cmp_profile, beta, "CMP with Letters", CLR_ACCENT2)
|
| |
|
| |
|
| | _draw_profile(ax3, gmp_profile, beta, "GMP with Letters", CLR_ACCENT3)
|
| | ax3.set_ylabel("")
|
| |
|
| | fig.suptitle("Datapoints β CMP β GMP (Ξ² = 1)",
|
| | fontsize=14, fontweight="bold", color=CLR_FG, y=1.02)
|
| | fig.tight_layout()
|
| | fig.savefig(path, dpi=CHART_DPI, bbox_inches="tight")
|
| | plt.close(fig)
|
| | print(f"[OK] {path}")
|
| |
|
| |
|
| | def chart_updown_footprint(updown_profile, beta,
|
| | path="fig_updown_footprint.png"):
|
| | """Dual horizontal bar chart: down-bins (left/red) vs up-bins (right/teal)."""
|
| | if not updown_profile:
|
| | return
|
| |
|
| | CLR_UP = "#00897b"
|
| | CLR_DOWN = "#e53935"
|
| |
|
| | b_min = min(updown_profile.keys())
|
| | b_max = max(updown_profile.keys())
|
| |
|
| | bin_labels = []
|
| | up_vals = []
|
| | down_vals = []
|
| | delta_vals = []
|
| | for b in range(b_min, b_max + 1):
|
| | p_from = b * beta
|
| | p_until = (b + 1) * beta
|
| | bin_labels.append(f"{int(p_from)}-{int(p_until)}")
|
| | info = updown_profile.get(b, {"labels": [], "up": 0, "down": 0})
|
| | up_vals.append(info["up"])
|
| | down_vals.append(info["down"])
|
| | delta_vals.append(info["up"] - info["down"])
|
| |
|
| | y_pos = list(range(len(bin_labels)))
|
| | max_val = max(max(up_vals, default=1), max(down_vals, default=1), 1)
|
| |
|
| | fig, ax = plt.subplots(figsize=(8, 5.5))
|
| | _apply_style(ax, "Up/Down-Bin Footprint Profile (GMP-based)")
|
| |
|
| |
|
| | bars_down = ax.barh(y_pos, [-d for d in down_vals], color=CLR_DOWN,
|
| | edgecolor="white", linewidth=0.5, height=0.65,
|
| | alpha=0.85, label="Down-bin")
|
| |
|
| | bars_up = ax.barh(y_pos, up_vals, color=CLR_UP,
|
| | edgecolor="white", linewidth=0.5, height=0.65,
|
| | alpha=0.85, label="Up-bin")
|
| |
|
| |
|
| | for i, (dv, uv, deltav) in enumerate(zip(down_vals, up_vals, delta_vals)):
|
| | if dv > 0:
|
| | ax.text(-dv - 0.15, i, str(dv), va="center", ha="right",
|
| | fontsize=7, color=CLR_DOWN, fontweight="bold")
|
| | if uv > 0:
|
| | ax.text(uv + 0.15, i, str(uv), va="center", ha="left",
|
| | fontsize=7, color=CLR_UP, fontweight="bold")
|
| |
|
| | delta_color = CLR_UP if deltav > 0 else (CLR_DOWN if deltav < 0 else CLR_MUTED)
|
| | delta_str = f"{deltav:+d}" if deltav != 0 else "0"
|
| | ax.text(max_val + 1.0, i, f"\u0394={delta_str}", va="center", ha="left",
|
| | fontsize=6.5, color=delta_color)
|
| |
|
| | ax.set_yticks(y_pos)
|
| | ax.set_yticklabels(bin_labels, fontsize=7)
|
| | ax.set_xlabel("Stacks", fontsize=9)
|
| | ax.set_ylabel("Price Bin (USD)", fontsize=9)
|
| | ax.axvline(0, color=CLR_FG, linewidth=0.6)
|
| | ax.set_xlim(-max_val - 1.5, max_val + 2.5)
|
| | ax.legend(loc="lower right", fontsize=8)
|
| |
|
| | fig.tight_layout()
|
| | fig.savefig(path, dpi=CHART_DPI, bbox_inches="tight")
|
| | plt.close(fig)
|
| | print(f"[OK] {path}")
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | def main():
|
| | out_dir = os.path.dirname(os.path.abspath(__file__))
|
| |
|
| |
|
| | cmp = build_cmp(DATAPOINTS, BIN_SIZE)
|
| | gmp = build_gmp(DATAPOINTS, BIN_SIZE)
|
| | updown = build_updown_profile(DATAPOINTS, BIN_SIZE)
|
| |
|
| |
|
| | write_datapoints_csv(DATAPOINTS, os.path.join(out_dir, "datapoints.csv"))
|
| | write_profile_csv(cmp, BIN_SIZE, os.path.join(out_dir, "cmp_profile.csv"))
|
| | write_profile_csv(gmp, BIN_SIZE, os.path.join(out_dir, "gmp_profile.csv"))
|
| | write_updown_profile_csv(updown, gmp, BIN_SIZE,
|
| | os.path.join(out_dir, "updown_profile.csv"))
|
| |
|
| |
|
| | if HAS_MPL:
|
| | chart_price_scatter(
|
| | DATAPOINTS, os.path.join(out_dir, "fig_price_scatter.png"))
|
| | chart_profile(
|
| | cmp, BIN_SIZE, os.path.join(out_dir, "fig_cmp_profile.png"),
|
| | "Conventional Market Profile (CMP)", CLR_ACCENT2)
|
| | chart_profile(
|
| | gmp, BIN_SIZE, os.path.join(out_dir, "fig_gmp_profile.png"),
|
| | "Gap-Filled Market Profile (GMP)", CLR_ACCENT3)
|
| | chart_cmp_vs_gmp(
|
| | cmp, gmp, BIN_SIZE,
|
| | os.path.join(out_dir, "fig_cmp_vs_gmp.png"))
|
| | chart_combined_3panel(
|
| | DATAPOINTS, cmp, gmp, BIN_SIZE,
|
| | os.path.join(out_dir, "fig_combined_3panel.png"))
|
| | chart_updown_footprint(
|
| | updown, BIN_SIZE,
|
| | os.path.join(out_dir, "fig_updown_footprint.png"))
|
| |
|
| |
|
| | print("\nββ CMP Profile ββ")
|
| | b_min = min(cmp.keys())
|
| | b_max = max(cmp.keys())
|
| | for b in range(b_min, b_max + 1):
|
| | info = cmp.get(b, {"labels": [], "count": 0})
|
| | grp = "".join(sorted(info["labels"]))
|
| | print(f" Bin {b - b_min + 1}: {int(b * BIN_SIZE)}β{int((b+1) * BIN_SIZE)} "
|
| | f"group={grp or 'β':6s} stacks={info['count']}")
|
| |
|
| | print("\nββ GMP Profile ββ")
|
| | b_min = min(gmp.keys())
|
| | b_max = max(gmp.keys())
|
| | for b in range(b_min, b_max + 1):
|
| | info = gmp.get(b, {"labels": [], "count": 0})
|
| | grp = "".join(sorted(info["labels"]))
|
| | print(f" Bin {b - b_min + 1}: {int(b * BIN_SIZE)}β{int((b+1) * BIN_SIZE)} "
|
| | f"group={grp or 'β':6s} stacks={info['count']}")
|
| |
|
| | print("\nββ Up/Down-Bin Footprint Profile ββ")
|
| | b_min = min(updown.keys())
|
| | b_max = max(updown.keys())
|
| | for b in range(b_min, b_max + 1):
|
| | info = updown.get(b, {"labels": [], "up": 0, "down": 0})
|
| | grp = "".join(info["labels"])
|
| | delta = info["up"] - info["down"]
|
| | print(f" Bin {b - b_min + 1}: {int(b * BIN_SIZE)}β{int((b+1) * BIN_SIZE)} "
|
| | f"group={grp or 'β':6s} up={info['up']} down={info['down']} "
|
| | f"delta={delta:+d}")
|
| |
|
| |
|
| | if __name__ == "__main__":
|
| | main()
|
| |
|