Bot Auto Trading

Global State trong Bot Python Hedging: Cấu trúc dữ liệu & Quản lý trạng thái toàn cục

01/08/2026 Đặng Trí Thanh 387 lượt xem ~19 phút đọc
Mục lục bài viết
    Global State trong Bot Python Hedging: Cấu trúc dữ liệu & Quản lý trạng thái toàn cục

    Global State trong Bot Python Hedging: Cấu trúc dữ liệu & Quản lý trạng thái toàn cục

    Một bot Hedging bằng Python không phải bot 1 lệnh – 1 chỉ báo. Trong một vòng loop, bot phải trả lời nhanh hàng loạt câu hỏi: tài khoản đang có bao nhiêu BUY/SELL, cụm XAUUSD magic này đang Surplus hay Hedge, equity hiện tại so với lúc mở cụm ra sao, ticket nào thuộc Level 3… Nếu không có một Global State (kho trạng thái toàn cục) đúng chuẩn, bot sẽ loay hoay, gọi MT5 lặp lại, và dễ đưa quyết định sai.

    Bài viết này giải thích cấu trúc dữ liệu và cách quản lý trạng thái toàn cục cho bot Python Hedging — nội dung Buổi 1 của khóa Lập trình Bot Python nâng cao Hedging & Tự động hóa tại Hướng Nghiệp Dữ Liệu.

    🎓 Khai giảng & Lịch học

    • Khai giảng: Thứ 7, 01/08/2026
    • Lịch học: 15h00 – 17h00 các ngày Thứ 7 & Chủ nhật hàng tuần
    • Hình thức: học trực tuyến (Zoom) + tài liệu, bài tập, hỗ trợ qua nhóm lớp
    • Khóa học phù hợp: học viên đã có nền tảng Python & bot MT5 (hoặc đã học các khóa Bot Auto Trading tại HNDL)

    📞 Đăng ký / tư vấn: Hotline/Zalo 0934 145 100 — hoặc xem chi tiết tại huongnghiepdulieu.com/lap-trinh-python-nang-cao-hedging-tu-dong-hoa

    1. Vì sao Global State quan trọng với bot Hedging?

    Bot Hedging cần trả lời nhanh các câu hỏi thực chiến:

    Câu hỏi thực chiến Nếu không có Global State
    Tài khoản đang có bao nhiêu BUY / SELL? Gọi MT5 lặp lại → chậm, lệch số
    Cụm XAUUSD magic 202601 đang Surplus hay Hedge? Mỗi hàm tự tính → kết quả khác nhau
    Equity hiện tại vs Equity lúc mở cụm? Không nhớ → Guard sai
    Ticket nào thuộc Level 3? Quên comment/magic → đóng nhầm lệnh

    Nguyên tắc khóa học:

    MT5 là nguồn sự thật của lệnh. Global State là nguồn sự thật của logic bot.

    Mỗi tick/loop: sync từ MT5 → cập nhật State → quyết định Surplus/Hedge/Sweep → gửi lệnh → cập nhật lại State.

    2. Bridge từ khóa MT5 — Global Variable vs Global State

    MT5 Global Variable (ổ cứng) vs Global State Python (RAM) — sau restart phải sync lại
    MT5 Global Variable (ổ cứng) vs Global State Python (RAM) — sau restart phải sync lại

    Học viên đã học khóa MT5 biết Terminal Global Variables (GlobalVariableSet/Get) lưu vào gvariable.dat — sống sót khi tắt Terminal / mất điện (nếu ghi kịp xuống đĩa).

    Khóa MT5 Khóa Python
    “Nhớ dài hạn” mặc định GV Terminal trên ổ cứng Không tự có — State nằm trong RAM process
    Restart / crash Đọc lại GV + quét lệnh Process chết → State mất hết
    Việc phải làm GlobalVariableGet + rebuild cache sync_from_mt5() lại (bắt buộc); tùy chọn save/load JSON/SQLite ở buổi sau

    Hệ quả thực chiến: Bot Python restart giữa cụm lệnh không được tin State cũ trong biến. Luôn coi MT5 positions là chuẩn, rebuild Cluster/Surplus từ sync mới.

    Buổi 1 chỉ xây State đúng trong phiên chạy. Persistence (ghi file/DB) sẽ bổ sung khi vào vận hành 24/7 — tinh thần giống Buổi 12 khóa MT5 (VPS), nhưng lớp lưu khác.

    3. Các thực thể dữ liệu cốt lõi

    3.1. Position (1 lệnh đang mở)

    ticket, symbol, magic, comment
    side (BUY/SELL), volume, price_open
    sl, tp, profit, swap, time_open
    level (X-Level suy ra từ comment hoặc index)
    cluster_id (gom theo symbol+magic hoặc symbol+magic+side)
    

    3.2. Cluster (1 “đám” lệnh cùng chiến lược)

    cluster_id
    symbol, magic
    buy_volume, sell_volume
    net_volume = buy - sell
    avg_buy, avg_sell
    surplus_side, surplus_volume
    state: (FSM — Buổi 7; thống nhất với khóa MT5)
    opened_equity, max_dd_pct
    

    Tên trạng thái FSM: khóa MT5 dùng WAITING | TRADING | LOCKDOWN | SWEEPING | PANIC. Khóa Python sẽ thống nhất cùng bộ tên ở Buổi 7 (FSM). Trong tài liệu nháp có thể gặp IDLE/ATTACK/HEDGE — coi là alias tạm, không dùng song song trong code production.

    3.3. AccountSnapshot (ảnh chụp tài khoản)

    balance, equity, margin, free_margin
    margin_level, open_positions
    ts (timestamp sync)
    

    3.4. BotConfig (cấu hình bất biến trong phiên)

    symbols, magics
    step_points, x_levels
    lot_base, lot_factor
    max_dd_pct, max_spread
    guard_enabled
    

    4. Chọn cấu trúc dữ liệu nào?

    Tra cứu O(1) bằng dict[ticket] thay vì quét list mỗi lần
    Tra cứu O(1) bằng dict[ticket] thay vì quét list mỗi lần
    Nhu cầu Cấu trúc đề xuất Lý do
    1 position @dataclass Rõ field, dễ đọc, type hint
    Tra cứu nhanh theo ticket dict[int, Position] O(1) lookup
    Lọc theo symbol/magic index phụ dict[tuple, set[int]] Tránh scan toàn bộ mỗi lần
    Cấu hình dataclass(frozen=True) hoặc TypedDict Ít bị sửa nhầm runtime
    Snapshot phục vụ dashboard dict / JSON-serializable Đẩy ra UI/Telegram dễ

    Anti-pattern cần tránh

    1. Biến global rải rác: positions = [], equity = 0 ở nhiều file.
    2. List position rồi for tìm ticket mỗi lần.
    3. Nhét mọi thứ vào 1 dict khổng lồ không schema.
    4. Gọi MT5 trực tiếp trong mọi hàm nghiệp vụ (không qua State).

    Anti-pattern: nhìn trước — sau

    Anti-pattern 1: scan list mỗi lần cần 1 lệnh

    # ❌ BAD
    for p in positions:                # positions là list
        if p["ticket"] == 1250:
            return p
    # Mỗi tick mà làm thế này với vài lệnh thì vẫn chạy, nhưng khi 50 lệnh +
    # nhiều module gọi, rõ ràng lãng phí và dễ sót ticket.
    
    # ✅ GOOD
    return gs.positions.get(1250)
    

    Anti-pattern 2: gọi MT5 trong mọi hàm nghiệp vụ

    # ❌ BAD — hàm quyết định tự hỏi MT5
    def should_hedge():
        positions = mt5.positions_get()      # gọi broker mỗi lần
        ...                                  # rồi tự đếm
    # Rủi ro: nhiều hàm gọi → chậm, giới hạn API, kết quả "thời điểm khác nhau".
    
    # ✅ GOOD — MT5 chỉ gọi ở 1 nơi (SyncEngine), mọi hàm đọc State
    def should_hedge(gs: GlobalState):
        cl = gs.get_cluster("XAUUSD#202601")
        return cl.surplus_volume >= gs.config.lot_base
    

    Anti-pattern 3: “god dict” không schema

    # ❌ BAD
    state = {"pos": [...], "eq": 0, "cfg": {...}}
    state["eq"] = mt5.account_info().equity   # ai cũng ghi, không kiểm soát
    
    # ✅ GOOD — dataclass + method
    gs.update_account(snapshot)               # có lock, có chuẩn hoá
    

    Anti-pattern 4: lọc theo comment bằng chuỗi trần rải rác

    # ❌ BAD — magic "202601" nhập tay ở 5 chỗ, sửa 1 chỗ quên chỗ kia
    # ✅ GOOD — magic nằm trong BotConfig, cluster_id sinh từ helper
    def make_cluster_id(symbol: str, magic: int) -> str:
        return f"{symbol}#{magic}"
    

    5. Mô hình Global State (Singleton có kiểm soát)

    ┌─────────────────────────────────────────────┐
    │                 GlobalState                 │
    │  config: BotConfig                          │
    │  account: AccountSnapshot                   │
    │  positions: dict[ticket -> Position]        │
    │  clusters:  dict[cluster_id -> Cluster]     │
    │  flags:     running, panic, last_error      │
    │  lock:      threading.RLock()  (chuẩn bị Buổi 6) │
    └─────────────────────────────────────────────┘
             ▲                ▲
             │ read           │ write (qua method)
       Strategy / Guard    SyncEngine / OrderRouter
    

    Quy tắc vàng

    1. Không gán trực tiếp field lung tung từ bên ngoài.
    2. Mọi thay đổi đi qua method: upsert_position(), remove_position(), rebuild_clusters().
    3. Đọc nhiều, ghi ít, ghi có khóa (lock) khi vào đa luồng.
    4. Sau mỗi sync MT5: rebuild derived metrics (net, surplus, avg).

    6. Derived State — tính toán từ dữ liệu gốc

    Tính net_volume và surplus từ các vị thế thực của cluster
    Tính net_volume và surplus từ các vị thế thực của cluster

    Dữ liệu gốc (source): ticket, side, volume, price_open…
    Dữ liệu suy ra (derived): không lưu tay nếu có thể tái tính ổn định.

    net_volume = buy_volume - sell_volume
    surplus_volume = abs(net_volume)
    surplus_side = "BUY" if net_volume > 0 else ("SELL" if net_volume < 0 else "NONE")
    

    Ví dụ thực chiến

    • BUY 0.30 + SELL 0.10 → net = +0.20 → Surplus BUY 0.20 → cần cân nhắc Hedge SELL.
    • BUY 0.20 + SELL 0.20 → net = 0 → có thể Sweep / chốt cụm.

    Buổi 5 sẽ dùng các field này cho logic Tấn công/Phòng thủ. Buổi 1 chỉ cần tính đúng và lưu đúng chỗ.

    7. Đồng bộ State với MT5 (luồng chuẩn)

    Luồng đồng bộ MT5 → GlobalState → Strategy → OrderRouter
    Luồng đồng bộ MT5 → GlobalState → Strategy → OrderRouter
    1. positions_get() / orders_get() từ MT5
    2. Lọc theo magic + symbol thuộc bot
    3. upsert / remove trong GlobalState
    4. rebuild_clusters()
    5. refresh AccountSnapshot
    6. Strategy đọc State → quyết định
    7. Gửi lệnh → (tuỳ chọn) optimistic update + sync lại
    

    Lưu ý: Optimistic update giúp UI mượt, nhưng luôn có 1 lần sync “cứng” sau lệnh để tránh State ảo.

    7.1. Mở rộng State cho nhiều cặp & nhiều magic

    Trong thực chiến, một tài khoản có thể chạy song song nhiều cụm: XAUUSD magic 202601 (chiến lược chính), XAUUSD magic 202602 (chiến lược phụ), XAGUSD magic 202601… Global State phải phân biệt rõ từng cụm.

    Quy ước cluster_id = f"{symbol}#{magic}" giúp tránh nhầm lẫn giữa “cùng symbol khác magic” và “cùng magic khác symbol”.

    def all_clusters_for_symbol(gs, symbol: str) -> list[Cluster]:
        with gs._lock:
            return [c for c in gs.clusters.values() if c.symbol == symbol]
    
    def total_net_for_magic(gs, magic: int) -> float:
        with gs._lock:
            return round(sum(c.net_volume for c in gs.clusters.values()
                             if c.magic == magic), 8)
    

    Khi nào nên tách cluster theo side?

    • Nếu chiến lược “BUY-ladder” và “SELL-ladder” là 2 kế hoạch độc lập trên cùng symbol+magic, có thể đặt cluster_id = f"{symbol}#{magic}#{side}".
    • Mặc định khóa học dùng symbol#magic (gộp cả 2 chiều), rồi tính Surplus — đơn giản, đúng tinh thần Hedge.
    • Quyết định này nên được viết rõ trong BotConfig (thêm field cluster_mode: str = "by_magic") thay vì rải rác trong code.

    Đọc State theo cụm — bảng quyết định nhanh

    Tình huống State Hành động gợi ý
    surplus_volume == 0 và đủ lệnh Chờ/cân nhắc Sweep chốt cụm
    Surplus BUY lớn, giá giảm Cân nhắc Hedge SELL đối trọng
    Drawdown ≥ max_dd_pct Chuyển cụm sang PANIC
    Cụm không có lệnh Xoá khỏi clusters để tránh rác

    8. Demo code thực tế

    Model Position / Cluster bằng @dataclass

    from dataclasses import dataclass
    
    @dataclass
    class Position:
        ticket: int
        symbol: str
        magic: int
        side: str        # BUY / SELL
        volume: float
        price_open: float
        comment: str = ""
        profit: float = 0.0
        level: int = 0
    

    GlobalState singleton + method cập nhật

    import threading
    
    class GlobalState:
        _instance = None
        _init_lock = threading.Lock()
    
        def __new__(cls):
            with cls._init_lock:
                if cls._instance is None:
                    obj = super().__new__(cls)
                    obj._ready = False
                    cls._instance = obj
                return cls._instance
    
        def __init__(self):
            if getattr(self, "_ready", False):
                return
            self._lock = threading.RLock()
            self.config = BotConfig()
            self.account = AccountSnapshot()
            self.positions: dict[int, Position] = {}
            self.clusters: dict[str, Cluster] = {}
            self.running = True
            self.last_error = None
            self._ready = True
    
        # --- ghi dữ liệu có kiểm soát ---
    
        def upsert_position(self, pos: Position):
            with self._lock:
                self.positions[pos.ticket] = pos
                self._rebuild_clusters_unlocked()
    
        def remove_position(self, ticket: int):
            with self._lock:
                self.positions.pop(ticket, None)
                self._rebuild_clusters_unlocked()
    
        def set_account(self, snap: AccountSnapshot):
            with self._lock:
                self.account = snap
    
        # --- đọc tiện ích ---
    
        def get_position(self, ticket: int) -> Position | None:
            with self._lock:
                return self.positions.get(ticket)
    
        def get_cluster(self, cluster_id: str) -> Cluster | None:
            with self._lock:
                return self.clusters.get(cluster_id)
    
        def get_positions_by_magic(self, magic: int) -> list[Position]:
            with self._lock:
                return [p for p in self.positions.values() if p.magic == magic]
    
        def snapshot(self) -> dict:
            """Trả bản copy JSON-friendly cho UI / Telegram / dashboard."""
            with self._lock:
                return {
                    "account": {
                        "balance": self.account.balance,
                        "equity": self.account.equity,
                        "free_margin": self.account.free_margin,
                        "margin_level": self.account.margin_level,
                    },
                    "positions": [
                        {
                            "ticket": p.ticket, "symbol": p.symbol,
                            "magic": p.magic, "side": p.side,
                            "volume": p.volume, "profit": p.profit,
                            "level": p.level,
                        }
                        for p in self.positions.values()
                    ],
                    "clusters": [
                        {
                            "cluster_id": c.cluster_id, "symbol": c.symbol,
                            "net_volume": c.net_volume,
                            "surplus_side": c.surplus_side,
                            "surplus_volume": c.surplus_volume,
                            "state": c.state,
                        }
                        for c in self.clusters.values()
                    ],
                    "ts": time.time(),
                }
    
        # --- rebuild derived ---
    
        def _rebuild_clusters_unlocked(self):
            buckets = {}
            for p in self.positions.values():
                cid = make_cluster_id(p.symbol, p.magic)
                buckets.setdefault(cid, []).append(p)
            rebuilt = {}
            for cid, lst in buckets.items():
                buy = sum(p.volume for p in lst if p.side == "BUY")
                sell = sum(p.volume for p in lst if p.side == "SELL")
                net = round(buy - sell, 8)
                avg_buy = (sum(p.volume * p.price_open for p in lst if p.side == "BUY")
                           / buy) if buy else 0.0
                avg_sell = (sum(p.volume * p.price_open for p in lst if p.side == "SELL")
                            / sell) if sell else 0.0
                rebuilt[cid] = Cluster(
                    cluster_id=cid,
                    symbol=lst[0].symbol,
                    magic=lst[0].magic,
                    buy_volume=buy, sell_volume=sell, net_volume=net,
                    surplus_side=("BUY" if net > 1e-8
                                  else "SELL" if net < -1e-8 else "NONE"),
                    surplus_volume=abs(net),
                    avg_buy=avg_buy, avg_sell=avg_sell,
                    tickets=[p.ticket for p in lst],
                )
            self.clusters = rebuilt
    
        # --- sync từ MT5 (nút sống còn sau restart) ---
    
        def sync_from_mt5(self):
            """Gọi khi khởi động hoặc sau lệnh để chắc chắn State = MT5."""
            with self._lock:
                fresh = {}                       # quét positions thật từ MT5
                for pos in read_positions_from_mt5():   # wrapper đọc broker
                    if pos.magic in self.config.magics:
                        fresh[pos.ticket] = pos
                self.positions = fresh
                self._rebuild_clusters_unlocked()
                self.set_account(read_account_from_mt5())
    

    Tại sao sync_from_mt5() là hàm “cứu mạng”?

    Python process chết (crash, restart VPS, đổi code) → State trong RAM bay sạch. Nhưng MT5 vẫn giữ nguyên các lệnh thật. Gọi sync_from_mt5() ngay khi khởi động sẽ tái dựng toàn bộ positions + clusters + account từ broker — bot “tỉnh dậy” đúng tình trạng cụm lệnh, không đoán mò.

    GlobalState hoàn chỉnh: set_account, get_cluster, snapshot, sync_from_mt5

    class GlobalState:
        def __init__(self):
            self._lock = threading.RLock()
            self.config = BotConfig()
            self.account = AccountSnapshot()
            self.positions: dict[int, Position] = {}
            self.clusters: dict[str, Cluster] = {}
            self.running = True
            self.last_error = None
    
        # --- ghi dữ liệu có kiểm soát ---
    
        def upsert_position(self, pos: Position):
            with self._lock:
                self.positions[pos.ticket] = pos
                self._rebuild_clusters_unlocked()
    
        def remove_position(self, ticket: int):
            with self._lock:
                self.positions.pop(ticket, None)
                self._rebuild_clusters_unlocked()
    
        def set_account(self, snap: AccountSnapshot):
            with self._lock:
                self.account = snap
    
        # --- đọc tiện ích ---
    
        def get_position(self, ticket: int) -> Position | None:
            with self._lock:
                return self.positions.get(ticket)
    
        def get_cluster(self, cluster_id: str) -> Cluster | None:
            with self._lock:
                return self.clusters.get(cluster_id)
    
        def get_positions_by_magic(self, magic: int) -> list[Position]:
            with self._lock:
                return [p for p in self.positions.values() if p.magic == magic]
    
        def snapshot(self) -> dict:
            """Trả bản copy JSON-friendly cho UI / Telegram / dashboard."""
            with self._lock:
                return {
                    "account": {
                        "balance": self.account.balance,
                        "equity": self.account.equity,
                        "free_margin": self.account.free_margin,
                        "margin_level": self.account.margin_level,
                    },
                    "positions": [
                        {
                            "ticket": p.ticket, "symbol": p.symbol,
                            "magic": p.magic, "side": p.side,
                            "volume": p.volume, "profit": p.profit,
                            "level": p.level,
                        }
                        for p in self.positions.values()
                    ],
                    "clusters": [
                        {
                            "cluster_id": c.cluster_id, "symbol": c.symbol,
                            "net_volume": c.net_volume,
                            "surplus_side": c.surplus_side,
                            "surplus_volume": c.surplus_volume,
                            "state": c.state,
                        }
                        for c in self.clusters.values()
                    ],
                    "ts": time.time(),
                }
    
        # --- rebuild derived ---
    
        def _rebuild_clusters_unlocked(self):
            buckets = {}
            for p in self.positions.values():
                cid = make_cluster_id(p.symbol, p.magic)
                buckets.setdefault(cid, []).append(p)
            rebuilt = {}
            for cid, lst in buckets.items():
                buy = sum(p.volume for p in lst if p.side == "BUY")
                sell = sum(p.volume for p in lst if p.side == "SELL")
                net = round(buy - sell, 8)
                avg_buy = (sum(p.volume * p.price_open for p in lst if p.side == "BUY")
                           / buy) if buy else 0.0
                avg_sell = (sum(p.volume * p.price_open for p in lst if p.side == "SELL")
                            / sell) if sell else 0.0
                rebuilt[cid] = Cluster(
                    cluster_id=cid,
                    symbol=lst[0].symbol,
                    magic=lst[0].magic,
                    buy_volume=buy, sell_volume=sell, net_volume=net,
                    surplus_side=("BUY" if net > 1e-8
                                  else "SELL" if net < -1e-8 else "NONE"),
                    surplus_volume=abs(net),
                    avg_buy=avg_buy, avg_sell=avg_sell,
                    tickets=[p.ticket for p in lst],
                )
            self.clusters = rebuilt
    
        # --- sync từ MT5 (nút sống còn sau restart) ---
    
        def sync_from_mt5(self):
            """Gọi khi khởi động hoặc sau lệnh để chắc chắn State = MT5."""
            with self._lock:
                fresh = {}                       # quét positions thật từ MT5
                for pos in read_positions_from_mt5():   # wrapper đọc broker
                    if pos.magic in self.config.magics:
                        fresh[pos.ticket] = pos
                self.positions = fresh
                self._rebuild_clusters_unlocked()
                self.set_account(read_account_from_mt5())
    

    9. Xử lý lỗi & “sync cứng” sau lệnh

    Trong vận hành, State có thể lệch MT5 vì nhiều lý do:

    • Lệnh bị từ chối (requote, margin, spread cao) nhưng code “tưởng” đã vào.
    • Lệnh được mở bằng tay trên MT5 (không qua bot).
    • Bot đặt TP/SL, MT5 tự đóng lệnh khi chạm — bot chưa kịp cập nhật.
    • Disconnect broker, sync bỏ lỡ 1 vài lệnh.

    Quy trình phòng thủ:

    1. Sau mỗi lệnh gửi đi: chờ broker xác nhận (deal) → rồi gọi sync_from_mt5() lại.
    2. Định kỳ (ví dụ mỗi 30–60 giây hoặc mỗi tick thứ N): refresh nhẹ positions.
    3. Ghi last_error vào State để dashboard hiển thị trạng thái “bot đang bị lệch hay không”.
    4. Khi phát hiện len(gs.positions) ≠ số lệnh MT5: dừng quyết định mới, sync lại, log cảnh báo.

    Mẫu vòng lặp chính:

    while gs.running:
        try:
            gs.sync_from_mt5()          # 1) State = MT5
            signal = strategy.on_tick(gs, price)   # 2) đọc State
            if signal == "HEDGE_NEEDED":
                router.send_hedge(gs)   # 3) gửi lệnh
                gs.sync_from_mt5()      # 4) sync cứng lại
        except Exception as e:
            gs.last_error = repr(e)
            log.exception("loop error")
            time.sleep(1)
        time.sleep(0.05)
    

    Mẹo: đừng để sync_from_mt5() thất bại làm chết bot. Luôn có try/except và giữ State cũ để bot “đứng yên an toàn” thay vì hành động mù.

    10. Kiểm thử Global State

    Trước khi chạy live, hãy kiểm thử State bằng dữ liệu giả (không cần broker):

    def test_net_surplus():
        gs = GlobalState()
        gs.upsert_position(Position(1, "XAUUSD", 202601, "BUY", 0.30, 2400.0))
        gs.upsert_position(Position(2, "XAUUSD", 202601, "SELL", 0.10, 2405.0))
        cl = gs.get_cluster("XAUUSD#202601")
        assert cl.net_volume == 0.20
        assert cl.surplus_side == "BUY"
        assert cl.surplus_volume == 0.20
    
    def test_remove_rebuild():
        gs = GlobalState()
        gs.upsert_position(Position(1, "XAUUSD", 202601, "BUY", 0.30, 2400.0))
        gs.upsert_position(Position(2, "XAUUSD", 202601, "SELL", 0.30, 2405.0))
        gs.remove_position(2)
        cl = gs.get_cluster("XAUUSD#202601")
        assert cl.net_volume == 0.30
        assert cl.surplus_side == "BUY"
    
    def test_snapshot_json():
        gs = GlobalState()
        gs.upsert_position(Position(1, "XAUUSD", 202601, "BUY", 0.30, 2400.0))
        snap = gs.snapshot()
        import json
        json.dumps(snap)   # không được lỗi
    

    Kiểm thử giúp phát hiện sớm lỗi “tính Surplus sai” — lỗi mà khi live sẽ tốn tiền thật.

    11. Sai lầm phổ biến của người mới

    1. Tin State cũ sau restart — không sync từ MT5 → cụm lệnh tính sai từ đầu.
    2. Nhét volume bằng tay thay vì rebuild từ positions → lệch số giữa các module.
    3. Gọi MT5 trong Strategy — chậm, và mỗi hàm đọc tại thời điểm khác nhau.
    4. Không có lock rồi sau này thêm thread → data race khó tìm.
    5. Lưu surplus_side như state độc lập mà không có nguồn gốc rõ → khi đảo chiều net, quên cập nhật.
    6. Sử dụng nhiều tên trạng thái (IDLE, BUYING…) thay vì bộ FSM chuẩn → rối khi lên Buổi 7.

    12. Checklist trước khi kết thúc buổi

    • [ ] Phân biệt được Position / Cluster / Account / Config.
    • [ ] Biết vì sao dùng dict[ticket] thay vì chỉ list.
    • [ ] Hiểu Singleton GlobalState dùng method, không sửa field bừa.
    • [ ] Tính được net_volume, surplus_side, surplus_volume.
    • [ ] Nắm luồng sync MT5 → State → Strategy.
    • [ ] Phân biệt GV MT5 (ổ cứng) vs State Python (RAM — mất khi restart).
    • [ ] Biết tên FSM chuẩn sẽ thống nhất với khóa MT5 (WAITING…) ở Buổi 7.

    13. Bridge sang các buổi sau

    Buổi Liên kết với Global State
    2 · Cache & Pandas/NumPy Cache giá/OHLC cạnh State, không nhét history vào positions
    3 · Magic & Comment cluster_id/level phụ thuộc magic + comment chuẩn
    5 · Surplus & Hedge Đọc derived fields từ Cluster
    6 · Multi-thread RLock trong GlobalState thành bắt buộc
    7 · FSM cluster.state dùng bộ tên chuẩn MT5: WAITING/TRADING/LOCKDOWN/SWEEPING/PANIC
    9 · Guard Đọc account + max_dd_pct để cắt rủi ro

    14. Thuật ngữ nhanh

    Thuật ngữ Nghĩa ngắn
    Global State Kho trạng thái dùng chung toàn bot (RAM process Python)
    Global Variable (MT5) Biến Terminal lưu gvariable.dat — chống mất điện phía EA MQL5
    Source of truth Nơi dữ liệu được tin là chuẩn
    Derived metric Chỉ số suy ra từ dữ liệu gốc

    15. Kết luận

    Global State chính là “bộ não nhớ” của bot Hedging Python. Xây đúng cấu trúc dữ liệu (@dataclass + dict[ticket] + Singleton có kiểm soát) giúp mọi module — Strategy, Guard, SyncEngine, OrderRouter — đọc/ghi trạng thái thống nhất, tránh lệch cụm và quyết định sai. Đây là nền tảng cho toàn bộ khóa học.

    👉 Khóa học Lập trình Bot Python nâng cao Hedging & Tự động hóa tại HNDL: https://www.huongnghiepdulieu.com/lap-trinh-python-nang-cao-hedging-tu-dong-hoa/

    📞 Hotline/Zalo: 0934 145 100

    FAQ

    Q: Vì sao dùng dict[ticket] thay vì list?
    A: Tra cứu theo ticket là O(1), trong khi list phải quét toàn bộ — với hàng chục lệnh mỗi giây, dict nhanh và sạch hơn nhiều.

    Q: Global State mất khi restart Python có sao không?
    A: Không — vì MT5 là nguồn sự thật. Sau restart, gọi sync_from_mt5() để rebuild State từ positions thật.

    Q: Có cần lưu State ra file không?
    A: Ở Buổi 1 chưa cần. Khi vào vận hành 24/7, bổ sung save/load JSON/SQLite (buổi sau).

    Q: Surplus side tính thế nào?
    A: net = buy - sell; net > 0 → BUY, net < 0 → SELL, net = 0 → NONE. Surplus volume = abs(net).

    Q: Singleton có phải luôn tốt không?
    A: Với bot đơn process, singleton có kiểm soát + lock là hợp lý. Khi scale nhiều process, dùng shared store (DB/queue) — nâng cao.

    Q: Vì sao phải rebuild clusters sau mỗi upsert/remove mà không cộng dồn?
    A: Cộng dồn dễ sai số float và quên cập nhật khi lệnh đổi magic/comment. Rebuild từ positions là nguồn duy nhất, đảm bảo mọi nơi cùng một kết quả.

    Q: net_volume dùng số thập phân thế nào?
    A: Volume có tối đa 2–3 số lẻ tùy broker. Nên round(..., 8) để tránh sai số float, nhưng luôn so sánh Surplus bằng > 1e-8 thay vì != 0.

    Q: Khi nào cần snapshot cho dashboard?
    A: Mỗi lần UI/Telegram hỏi, gọi gs.snapshot() để lấy bản copy JSON — không truyền thẳng các object đang bị thread khác sửa.

    Q: Có nên tách GlobalState thành nhiều class nhỏ không?
    A: Tùy độ lớn dự án. Nếu dự án nhỏ, 1 class đủ. Nếu lớn, tách PositionStore, ClusterStore, AccountStore nhưng vẫn đi qua 1 facade GlobalState để giữ API thống nhất.

    Q: Học xong Buổi 1 cần tự viết lại toàn bộ không?
    A: Không. Có sẵn demo_position_models.py và demo_global_state.py trong tài liệu buổi học. Hãy đọc hiểu, chạy thử, rồi tự viết lại cluster_id(), upsert/remove, sync_from_mt5, set_account, get_cluster, snapshot để nhớ sâu — đó chính là bài tập Buổi 1.

    Kịch bản thực chiến: bot đang chạy mà “không biết mình đang có gì”

    Hình dung một buổi tối thị trường XAUUSD biến động mạnh. Bot Hedging Python mở ra một cụm lệnh:

    • Lệnh 1: BUY XAUUSD 0.30, magic 202601, ticket 1250.
    • Lệnh 2: SELL XAUUSD 0.10, magic 202601, ticket 1251.
    • Lệnh 3: SELL XAUUSD 0.20, magic 202601, ticket 1252.
    • Lệnh 4: BUY XAUUSD 0.05, magic 202602 (một chiến lược phụ), ticket 1253.

    Nếu mỗi hàm trong bot tự gọi positions_get() rồi tự đếm, rất dễ xảy ra:

    1. Hàm A đếm được 3 lệnh (lọc sai magic).
    2. Hàm B tính Surplus BUY 0.30, hàm C tính Surplus SELL 0.25.
    3. Guard thấy equity giảm 3% nhưng không biết lúc mở cụm equity là bao nhiêu.
    4. Khi cần đóng lệnh Level 3, bot không nhớ ticket nào thuộc Level 3.

    Kết quả: bot ra lệnh Hedge trùng, hoặc không Hedge đúng lúc, hoặc đóng nhầm lệnh. Không phải bot dốt, mà vì không có một Global State chuẩn duy nhất.

    Với Global State tốt, toàn bộ bot chỉ đọc một nguồn:

    • positions[1250] → BUY 0.30 magic 202601.
    • clusters["XAUUSD#202601"].surplus_side → BUY, surplus 0.15.
    • account.equity vs clusters["XAUUSD#202601"].opened_equity → Guard quyết định chuẩn.
    • positions[1250].level → 3, có trong danh sách đóng lệnh.

    Đây chính là lý do Buổi 1 bắt đầu bằng cấu trúc dữ liệu và quản lý trạng thái toàn cục, trước khi học Hedge, FSM hay Guard.


    🎓 Khai giảng & Lịch học khóa Lập Trình Bot Python Nâng Cao Hedging & Tự Động Hóa:
    Khai giảng Thứ 7 01/08/2026 · Lịch học 15h00 – 17h00 các ngày Thứ 7 & Chủ nhật hàng tuần (học online Zoom + tài liệu/bài tập).
    Đăng ký/tư vấn: 📞 Hotline/Zalo 0934 145 100 — chi tiết: huongnghiepdulieu.com/lap-trinh-python-nang-cao-hedging-tu-dong-hoa

    📌 Muốn làm chủ Bot Python Hedging thực chiến — từ cấu trúc dữ liệu, Global State, FSM đến Guard & vận hành tự động hóa?
    Khóa học Lập Trình Bot Python Nâng Cao Hedging & Tự Động Hóa tại Hướng Nghiệp Dữ Liệu hướng dẫn chi tiết từng buổi: cấu trúc dữ liệu, sync MT5, Surplus/Hedge, FSM, multi-thread, Guard rủi ro và vận hành bot trên tài khoản thật.

    Đọc thêm

    Đặng Trí Thanh

    Đặng Trí Thanh

    Giám đốc Công nghệ · DNT Digital · Giảng viên HNDL Hướng Nghiệp Dữ Liệu
    832 Bài viết
    15.4k Người theo dõi
    120k+ Lượt đọc

    Đặng Trí Thanh — Founder & CTO · Hướng Nghiệp Dữ Liệu - DNT Digital. Chuyên đào tạo và triển khai thực chiến Python, MT5 và hệ thống bot auto trading / IB cho học viên và doanh nghiệp.

    Bài viết liên quan

    DNT DIGITAL

    🤖 AI CODE: Vibe Code NEEDED! 🚀 🏦 SSI • DNSE • MT5 • Bitget ⚡

    Zalo 0934145100
    Khóa đào tạo
    Hỗ trợ

    Được học lại miễn phí · Hỗ trợ trọn đời · Cập nhật miễn phí

    Đội ngũ hỗ trợ

    Đặng Trí Thanh
    Đặng Trí Thanh
    Giám đốc Công nghệ DNT Digital
    Zalo 0934145100
    Mộng Cầm
    Mộng Cầm
    Hỗ trợ khách hàng · Huấn luyện viên
    Zalo 0927909257
    Khánh Linh
    Khánh Linh
    Hỗ trợ khách hàng · Huấn luyện viên
    Zalo 0927909582