English · Tiếng Việt · Tutorial dnse_auto_bot · Write a strategy
Official Python SDK for integrating with DNSE OpenAPI — REST trading, market data, broker APIs, WebSocket realtime, and derivative auto-trading helpers.
- Overview
- Project layout
- Quick start
- Environment variables
- DNSEClientV2
- Stock order flow
- DerivativeTrading
- DNSEClientV2 method catalog
- WebSocket
- Examples
- Portfolio risk
- Price & precision notes
- Installation
- Low-level SDK usage
- Dry run
- Full API docs
DNSE OpenAPI is an API-first trading platform for brokerage, trading, margin, and market data.
This repository provides:
| Layer | Module | Role |
|---|---|---|
| Official transport | dnse_sdk.dnse (DNSEClient / TradingClient) |
Signed REST + base WebSocket (do not modify) |
| High-level facade | dnse_auto_bot.sdk.DNSEClientV2 |
Recommended — REST + hardened WS + stock/derivative helpers |
| Order precision | dnse_auto_bot.sdk.order_utils |
Tick/lot, IMAP OTP, stock & derivative limits |
| Models / resources | dnse_auto_bot.sdk.models, resources, errors |
Opt-in Pydantic parse, resource namespaces, rate-limit errors |
| Engine | dnse_auto_bot.engine |
Config, strategy registry, multi-symbol bot, paper/backtest, alerts/metrics |
| Derivative bot package | dnse_auto_bot |
Public re-exports of SDK + engine |
API catalog: dnse_api.md · Tiếng Việt.
dnse_sdk/ # Official DNSE OpenAPI package (dnse_sdk/dnse) — transport only
dnse_auto_bot/
sdk/
client.py # DNSEClientV2 facade
trading.py # DerivativeTrading domain layer
order_utils.py # Tick/lot, IMAP OTP
hardened_ws.py # HardenedTradingClient (PONG / reconnect / rotate)
resources.py # client.accounts|orders|positions|market|registration
models/ # Pydantic REST + stream models (opt-in typed=True)
errors.py / enums.py / sanitize.py
engine/ # config, registry, bot, RiskGateway, OMS, paper, backtest, SWITCH
strategies/ # @register factories — see strategies/README.md
ops/ # watch_health (stale health.json TTL)
configs/ # default, paper, live_conservative, smc_paper, smc_live
tests/ # Unit + live integration (needs .env keys)
examples/ # Bot CLIs, OTP, TP/SL, smoke/reverify, SMC
dnse_api.md / dnse_api.vi.md # DNSEClientV2 method reference
dnse-documents-api.md # Live-verified request/response samples
examples/ # Stock / V2 suite scripts (repo root)
trading-api/ … # Also mirrored under dnse_sdk/ — raw REST / WS samples
requirements.txt # urllib3, websockets, certifi, python-dotenv, msgpack, pydantic
Copy the shared template (no secrets) then fill your keys:
cp .env.example .envMinimal required keys:
api-key=YOUR_API_KEY
secret-key=YOUR_API_SECRET
# Optional — required for auto OTP / place order
EMAIL_ADDRESS=you@gmail.com
EMAIL_PASSWORD=your_gmail_app_password
IMAP_HOST=imap.gmail.comBot defaults (paper mode, EMA, VN30F1M, …) are also listed in .env.example.
Full reference: Environment variables.
Never commit .env (gitignored). Commit / share .env.example only.
pip install -r requirements.txt(urllib3, websockets, certifi, python-dotenv, msgpack, pydantic>=2.0)
from dnse_auto_bot import DNSEClientV2
client = DNSEClientV2() # reads .env
status, accounts = client.get_accounts()
# Opt-in typed parse via resource facade:
status, accounts_m = client.accounts.list(typed=True)
status, quote = client.get_latest_quote("HPG", board_id="G1")
print(status, accounts, quote)from dnse_auto_bot import DerivativeTrading
trader = DerivativeTrading(default_symbol="VN30F1M")
print(trader.resolve("VN30F1M"))
# → contract=41I1G8000, trading=VN30F1M
trader.ensure_trading_token()
snap = trader.snapshot()
# trader.buy(price=snap["quote"]["ask"], quantity=1)Template: .env.example → copy to .env and fill secrets.
Engine JSON (dnse_auto_bot/configs/default.json) is the base; any set env var below overrides that config when load_config(apply_env=True).
| Variable | Required | Default | Description |
|---|---|---|---|
api-key |
Yes | — | DNSE OpenAPI key. Aliases: API_KEY, DNSE_API_KEY |
secret-key |
Yes | — | DNSE OpenAPI secret. Aliases: SECRET_KEY, DNSE_API_SECRET, api-secret |
| Variable | Required | Default | Description |
|---|---|---|---|
EMAIL_ADDRESS |
For auto OTP | — | Mailbox that receives DNSE OTP. Alias: IMAP_USER |
EMAIL_PASSWORD |
For auto OTP | — | IMAP password / Gmail App Password. Aliases: EMAIL_APP_PASSWORD, IMAP_PASSWORD |
IMAP_HOST |
No | imap.gmail.com |
IMAP server host |
IMAP_PORT |
No | 993 |
IMAP SSL port |
TEST_SEND_EMAIL_OTP |
No | — | 1 = feature-test suites may call send_email_otp |
TRADING_OTP / DNSE_OTP |
No | — | Paste OTP manually instead of reading IMAP (debug) |
Auto-bot token flow (canonical): send_email_otp → IMAP read OTP → create_trading_token.
Live/smoke always use this when obtain_token=true. Do not rely on SKIP_TOKEN / TRADING_TOKEN shortcuts for the bot (TokenManager no longer seeds from TRADING_TOKEN).
| Variable | Required | Default | Description |
|---|---|---|---|
BOT_CONFIG |
No | dnse_auto_bot/configs/default.json |
Path to engine JSON config |
BOT_MODE |
No | from JSON (paper) |
paper | live | smoke | forever (forever → live + run_forever) |
DERIV_SYMBOL |
No | from JSON | Futures symbolType (e.g. VN30F1M). Replaces config symbols with one symbol |
BOT_STRATEGY |
No | ema |
Registry: ema, ema_with_bracket, hold, smc_lq_ob, or custom @register(...) |
EMA_FAST |
No | 9 |
Fast EMA period (when strategy is EMA) |
EMA_SLOW |
No | 21 |
Slow EMA period |
OHLC_RESOLUTION |
No | 5 |
Bar size (minutes as string: 1, 5, 15, …) applied to all symbols |
OHLC_LOOKBACK_DAYS |
No | 5 |
OHLC history window for strategy context |
BOT_RISK_PROFILE |
No | balanced |
Portfolio preset — see Portfolio risk: conservative | balanced | aggressive | fixed_qty | custom |
BOT_QTY |
No | profile default | Default order quantity (CLI / risk override) |
BOT_MAX_QTY |
No | profile default | Max quantity per order |
BOT_REQUIRE_AFFORDABLE |
No | 1 |
0 = allow place even when qmax=0 (unsafe) |
BOT_USE_EXCHANGE_TP_SL |
No | live=1 |
1 = after fill, push nested SL/TP to DNSE pnl-configs + GET verify; 0 = bot-polled hard exits |
BOT_POLL_SECONDS |
No | 60 |
Seconds between evaluate loops |
BOT_MAX_LOOPS |
No | 1 |
Sync bot.run() loop count (run_forever ignores this) |
BOT_ENABLE_WS |
No | 1 |
0 = REST-only (no WebSocket) |
BOT_WS_MAX_RETRIES |
No | 0 |
WS reconnect attempts; 0 = unlimited |
BOT_INSTANCE_LOCK |
No | 1 |
0 = disable runtime/bot.lock single-instance guard |
BOT_EXECUTION_WORKERS |
No | 1 |
Async execution pipeline workers (run_forever) |
BOT_AGGRESSIVE_TICKS |
No | 0 |
Tick offset when building aggressive LO price |
BOT_PASSIVE |
No | 0 |
1 = use bid/ask passive price instead of aggressive |
BOT_PAPER_SLIPPAGE_TICKS |
No | 0 |
Simulated slippage (ticks) in paper mode |
BOT_OHLC_PERSIST |
No | 1 |
0 = disable candle cache runtime/ohlc/ |
BOT_OMS_RESUME |
No | 1 |
0 = skip OMS resume on startup |
| Variable | Required | Default | Description |
|---|---|---|---|
ALERTS_ENABLED |
No | 1 |
0 = log only, no Telegram/Discord posts |
ALERTS_MIN_LEVEL |
No | warning |
Minimum level: debug | info | warning | error | critical |
TELEGRAM_BOT_TOKEN |
No | — | Telegram BotFather token |
TELEGRAM_CHAT_ID |
No | — | Destination chat / channel id (inbound /status /kill /pause /switch only from this id) |
DISCORD_WEBHOOK_URL |
No | — | Discord incoming webhook URL |
| Variable | Used by | Description |
|---|---|---|
SKIP_LIVE_ORDER |
derivative_ema_strategy.py |
1 = signal/account checks only (no OTP / no order) |
FORCE_DERIV_ORDER |
derivative_ema_strategy.py |
1 = still post_order when qmax < 1 |
DERIV_QTY |
derivative_ema_strategy.py |
Quantity for that example (default 1) |
RUN_LIVE_ORDER_FLOW |
examples/order_flow.py |
1 = allow live stock order-flow path |
ORDER_SYMBOL / ORDER_SIDE / ORDER_PRICE / ORDER_QTY |
examples/place_order_recommended.py |
Stock demo order params |
DEBUG |
dnse.api |
true = verbose HTTP signing / debug |
DNSE_API_VERSION |
dnse.api |
Override API version header |
DATE_HEADER |
dnse.api |
Override date header name (default Date) |
Safe paper (no OTP, no WS):
BOT_MODE=paper
BOT_ENABLE_WS=0
BOT_MAX_LOOPS=1
BOT_USE_EXCHANGE_TP_SL=0
SKIP_LIVE_ORDER=1Live forever (needs ~35–40M remainSecure / qmax≥1 + IMAP email OTP):
BOT_MODE=forever
BOT_ENABLE_WS=1
BOT_RISK_PROFILE=conservative
BOT_QTY=1
BOT_REQUIRE_AFFORDABLE=1
BOT_USE_EXCHANGE_TP_SL=1
EMAIL_ADDRESS=you@gmail.com
EMAIL_PASSWORD=your_gmail_app_passwordDNSEClientV2 (dnse_auto_bot.sdk.client) is the recommended high-level entry point.
Full method list: dnse_api.md · Tiếng Việt.
Purpose
- One class for REST + WebSocket over official
dnse_sdk.dnse - Load
api-key/secret-keyfrom.env - Auto-resolve
account_no/investor_idfromget_accounts - Parse JSON (incl. sanitize broken
metadata); optionally raiseDNSEAPIErrorwhenraise_on_error=True - Auto-retry HTTP 429; classify auth / session-expired / rate-limit errors
set_trading_token+ auto-inject on order / PnL / close mutations- Stock & derivative helpers; resource namespaces (
client.accounts…)
Feature groups
| Group | Capabilities |
|---|---|
| Account | get_accounts, balances (stock/derivative/bond/egg), loan packages, PPSE, default_account_no / investor_id / custody_code |
| Trading REST | Paginated orders, detail/history, executions, positions, close price, corporate actions |
| Auth / token | Email OTP, IMAP, obtain_trading_token, set_trading_token |
| Stock order flow | Limits, normalize, build/place/modify/cancel_stock_order |
| Derivative helpers | Resolve VN30F1M, limits, build/place, close_position |
| Exchange TP/SL | Nested pnl-configs GET/POST, set_tp_sl_position, verify matcher |
| Market data | Instruments, secdef, trade/quote, expected price, foreign, OHLC, sessions, working dates |
| Resources (DX) | client.accounts / orders / positions / market / registration — optional typed=True |
| Broker | get_list_care_by (broker role) |
| WebSocket | HardenedTradingClient + full market/private channels; opt-in typed=True on subscribe |
from dnse_auto_bot import DNSEClientV2, DNSEAPIError, DNSERateLimitError
client = DNSEClientV2(raise_on_error=True)
try:
client.get_order_detail(order_id="1")
except DNSERateLimitError as e:
print("retry_after", e.retry_after)
except DNSEAPIError as e:
print(e.status, e.code, e.body)Helpers in DNSEClientV2 + dnse_auto_bot.sdk.order_utils for HOSE equities.
| Method | Description |
|---|---|
get_order_limits(symbol) |
Floor / ceiling / tick / lot from secdef |
normalize_order_price |
Display ↔ VND, align tick, clamp band |
normalize_order_quantity |
Align to lot (default 100) |
build_stock_order |
Build validated payload + PPSE warnings |
obtain_trading_token |
Send email OTP → IMAP read → trading token |
place_stock_order |
Full place pipeline |
modify_stock_order / cancel_stock_order |
Modify / cancel with error wrapping |
client = DNSEClientV2()
result = client.place_stock_order(
"HQC",
side="NB",
price=2.03, # display or VND (auto)
quantity=100,
obtain_token=True, # OTP via email
)
print(result["order_id"], result["warnings"])Price units (stocks)
| Source | Unit | Example HPG |
|---|---|---|
| Market data / secdef | Display (nghìn đồng) | 22.55 |
| Order / PPSE API | VND (đồng) | 22550 |
HOSE tick (VND): <10k → 10, <50k → 50, ≥50k → 100. Lot G1: 100.
DerivativeTrading (dnse_auto_bot.sdk.trading) is the recommended facade for derivative auto-trading bots.
DNSE uses two symbol forms for futures:
| Use case | Symbol form | Example |
|---|---|---|
| Orders, PPSE, latest quote/trade, secdef | Contract code | 41I1G8000 |
| OHLC REST, loanPackages query, many WS OHLC | symbolType | VN30F1M |
With DerivativeTrading, you only pass symbol="VN30F1M" — the class resolves and routes the correct code automatically.
from dnse_auto_bot import DerivativeTrading, RiskConfig
trader = DerivativeTrading(
default_symbol="VN30F1M",
risk=RiskConfig(
max_quantity=1,
default_quantity=1,
require_affordable=True, # block place if qmax < qty
dry_run=False,
),
)| Method | Description |
|---|---|
resolve(symbol) |
VN30F1M / 41I1G8000 → ResolvedSymbol (cached) |
contract_of(symbol) |
Exchange contract code |
trading_of(symbol) |
Friendly symbolType |
list_instruments(enrich_quotes=True) |
All FU/DVX futures + optional live quotes |
| Method | Description |
|---|---|
ensure_trading_token(...) |
Canonical: send email OTP → IMAP → trading token (process cache) |
trading_token |
Get / set current in-memory token |
| Method | Description |
|---|---|
get_limits(symbol) |
Floor / ceiling / tick 0.1 / lot 1 |
normalize_price(price, symbol) |
Align to derivative tick + band |
get_quote(symbol) |
BBO (bid / ask) |
get_trade(symbol) |
Last match |
get_last_price(symbol) |
Best available last price |
get_ohlc(symbol, resolution, lookback_days) |
Historical bars (DERIVATIVE) |
get_session() |
FIO trading session |
is_tradable(symbol) |
Halt / sanction check |
| Method | Description |
|---|---|
get_balances() |
remainSecure, usedSecure, … |
get_loan_package_id(symbol) |
Cached derivative loan package |
get_ppse(symbol, price) |
qmaxBuy / qmaxSell |
can_afford(side, qty, symbol, price) |
Affordability check |
check_risk(side, qty, ...) |
Hard risk gate list |
| Method | Description |
|---|---|
build_order(side, price, qty, symbol) |
Build payload (always uses contract code) |
place / buy / sell |
Place LO (NB/NS) |
modify(order_id, price=, quantity=) |
Replace order |
cancel(order_id) |
Cancel one order |
get_order / get_orders |
Detail / book (get_orders(..., raise_on_error=False) soft-fail) |
wait_order(order_id, timeout=) |
Poll until terminal |
cancel_open_orders(symbol=) |
Cancel working orders |
| Method | Description |
|---|---|
get_positions(raise_on_error=True) |
All derivative positions (False = soft-fail → []) |
get_position(symbol) |
Position for one symbol |
get_position_by_id(id) |
Detail by id |
set_tp_sl_position(id, …) |
Nested pnl-configs POST + optional GET verify |
get_tp_sl_position(id) |
GET current pnl-configs for a deal |
close_position(id) |
Close by id |
flatten(symbol=) |
Close all (optionally filtered) |
| Method | Description |
|---|---|
snapshot(symbol) |
One-shot state; soft-fail positions / orders so flaky 400s do not kill loops |
market_order_price(side, ...) |
Near-market LO suggestion |
passive_order_price(side, ...) |
Floor/ceiling (safe API smoke) |
events |
Internal event log |
| Method | Description |
|---|---|
ws_connect / ws_disconnect |
Lifecycle |
subscribe_quotes / subscribe_trades |
Contract symbols |
subscribe_ohlc |
symbolType (VN30F1M) |
subscribe_orders / subscribe_positions |
Private |
subscribe_order_event / subscribe_position_event |
Private events |
trader = DerivativeTrading(default_symbol="VN30F1M")
trader.ensure_trading_token()
# Place with friendly symbol only
trader.buy(price=1915.5, quantity=1, symbol="VN30F1M")
trader.sell(price=1916.0, quantity=1) # uses default_symbol
# Manage
orders = trader.get_orders()
trader.modify(order_id, price=1915.7)
trader.cancel(order_id)
trader.flatten("VN30F1M")| symbolType | Contract | Name |
|---|---|---|
| VN30F1M | 41I1G8000 | HĐTL VN30 1 tháng |
| VN30F2M | 41I1G9000 | HĐTL VN30 2 tháng |
| VN30F1Q | 41I1GC000 | HĐTL VN30 1 quý |
| VN30F2Q | 41I1H3000 | HĐTL VN30 2 quý |
| V100F1M | 41I2G8000 | HĐTL VN100 1 tháng |
| V100F2M | 41I2G9000 | HĐTL VN100 2 tháng |
| V100F1Q | 41I2GC000 | HĐTL VN100 1 quý |
| V100F2Q | 41I2H3000 | HĐTL VN100 2 quý |
Derivative precision: index points, tick 0.1, lot 1.
Typical VN30 initial margin ≈ price × 100_000 × initialRate (~35M+ VND / contract).
Complete tables live in dnse_api.md. Summary:
| Method | Notes |
|---|---|
get_accounts |
Investor + sub-accounts |
get_balances |
stock / derivative / bond / egg sections |
get_loan_packages(market_type, symbol) |
STOCK / DERIVATIVE |
get_ppse(...) |
Buying / selling power |
get_orders(..., page_index, page_size) |
Intraday book (paginated, API 2026-07-23+) |
get_order_detail / get_order_history |
Detail / history (order id is string) |
get_execution_detail |
Fills |
get_positions / get_position_by_id |
STOCK | DERIVATIVE |
get_pnl_configs_position / get_position_pnl_configs |
GET TP/SL (aliases) |
post_pnl_configs_position / post_position_pnl_configs |
POST TP/SL |
set_tp_sl_position / build_pnl_config_body / pnl_config_matches |
Nested OpenAPI + verify |
get_close_price / get_corporate_action_history |
Close / CA |
send_email_otp / create_trading_token / obtain_trading_token |
Email OTP auth |
set_trading_token |
Store token for mutations |
post_order / put_order / cancel_order |
Raw CRUD (POST /accounts/{accountNo}/orders) |
close_position |
DERIVATIVE only |
| Method | Notes |
|---|---|
get_instruments / list_stock_instruments |
Filters / equity pagination |
get_security_definition |
Band, halt flags |
get_latest_trade / get_latest_quote |
Latest tick / BBO |
get_trades / get_quotes / get_foreign_trading |
History (limit time range) |
get_expected_price |
ATO/ATC expected (changelog 2026-08-06) |
get_ohlc(bar_type, query) |
STOCK / DERIVATIVE |
get_working_dates |
Calendar |
get_lastest_session / get_trading_session |
Session (alias) |
get_list_care_by |
Broker-only |
st, bal = client.accounts.balances(typed=True) # AccountBalanceResponse
st, book = client.orders.list(market_type="DERIVATIVE", typed=True)Namespaces: client.accounts, client.orders, client.positions, client.market, client.registration.
Same as before: get_order_limits, build/place_*_order, resolve_derivative_instrument, …
For bots, prefer DerivativeTrading.
V2 uses HardenedTradingClient (unlimited reconnect when max_retries<=0, proactive PONG ~90s, soft rotate ~7h). Connect once, then subscribe:
import asyncio
from dnse_auto_bot import DNSEClientV2
async def main():
client = DNSEClientV2()
await client.ws_connect()
await client.subscribe_quotes(["HPG"], on_quote=print, board_id="G1")
await client.subscribe_trades(["HPG"], on_trade=print, typed=True) # StreamTrade
await asyncio.sleep(10)
await client.ws_disconnect()
asyncio.run(main())| Method | Channel purpose |
|---|---|
subscribe_trades |
Tick |
subscribe_trade_extra |
Tick + aggregates |
subscribe_quotes |
BBO / top price |
subscribe_ohlc |
Forming candle |
subscribe_ohlc_closed |
Closed candle |
subscribe_sec_def |
Security definition |
subscribe_expected_price |
ATO / ATC expected |
subscribe_foreign_trading |
Foreign flow |
subscribe_market_index |
Index |
subscribe_estimated_market_index |
Estimated index |
subscribe_session |
Session events |
| Method | Purpose |
|---|---|
subscribe_order_event |
Order updates |
subscribe_position_event |
Position updates |
subscribe_orders / subscribe_positions / subscribe_account |
Snapshots / streams |
subscribe_broker_order_event / subscribe_broker_position_event |
Broker role |
Also: on(event, handler), queue(event), unsubscribe(channel, symbols), ws_is_healthy().
Derivative WS via facade:
trader = DerivativeTrading(default_symbol="VN30F1M")
await trader.ws_connect()
await trader.subscribe_quotes() # → 41I1G8000
await trader.subscribe_ohlc() # → VN30F1MRun from repo root (with .env configured):
# Full V2 regression
python examples/test_all_dnse_client_v2.py
# Basics
python examples/basic_usage.py
python examples/order_flow.py
python examples/place_order_recommended.py
python examples/trading_flow_live_test.py
# Derivatives / auto-bot
python dnse_auto_bot/examples/derivative_ema_strategy.py
python dnse_auto_bot/examples/derivative_auto_trade_bot.py paper
python dnse_auto_bot/examples/smc_full_bot_example.py verify
python dnse_auto_bot/examples/test_otp_email_token_flow.py| File | Description |
|---|---|
examples/test_all_dnse_client_v2.py |
Live test all V2 REST + WS methods |
examples/basic_usage.py |
Short REST + WS demo |
examples/order_flow.py |
Stock tick/lot precision |
examples/place_order_recommended.py |
OTP → token → stock order |
examples/trading_flow_live_test.py |
Place/modify/cancel + error matrix |
dnse_auto_bot/examples/derivative_ema_strategy.py |
EMA walkthrough: OTP, nested TP/SL preview, soft-fail |
dnse_auto_bot/examples/derivative_auto_trade_bot.py |
Engine CLI (paper / live / smoke / forever) |
dnse_auto_bot/examples/smc_full_bot_example.py |
Full SMC strategy + verify pipeline |
dnse_auto_bot/examples/test_otp_email_token_flow.py |
Canonical email OTP → trading token |
dnse_auto_bot/examples/test_set_tp_sl_position.py |
Exchange pnl-configs set + verify |
dnse_auto_bot/examples/check_derivative_affordability.py |
qmax / margin estimate across FU symbols |
| Mode | Behavior |
|---|---|
paper |
OHLC + strategy + risk — no orders, no OTP, local PositionRisk |
smoke |
Far LO (floor/ceiling) to exercise email OTP / place / detail |
live |
Place/close on signals; default use_exchange_tp_sl after fill |
forever |
Long-running asyncio + WS + reconcile + token refresh |
python dnse_auto_bot/examples/derivative_auto_trade_bot.py paper
python dnse_auto_bot/examples/derivative_auto_trade_bot.py smoke
python dnse_auto_bot/examples/derivative_auto_trade_bot.py live
python dnse_auto_bot/examples/derivative_auto_trade_bot.py forever
python dnse_auto_bot/examples/smc_full_bot_example.py verifyEnv knobs: see Environment variables (or .env.example).
Common: BOT_MODE, DERIV_SYMBOL, BOT_STRATEGY, BOT_RISK_PROFILE, BOT_USE_EXCHANGE_TP_SL, BOT_ENABLE_WS, EMAIL_*.
Tutorial: dnse_auto_bot_tutorial.md · Engine: dnse_auto_system_engine.md · Write a strategy: dnse_auto_bot/strategies/README.md.
| Module | Role |
|---|---|
dnse_auto_bot/strategy.py |
Strategy ABC, TradeSignal (BUY/SELL/HOLD/CLOSE* → NB/NS), EmaStrategy, HoldStrategy |
dnse_auto_bot/strategies/ |
Registry factories (@register("ema"), …) — authoring guide |
dnse_auto_bot/risk.py |
Portfolio risk presets + kill-switch / sizing — see Portfolio risk |
dnse_auto_bot/runtime_helpers.py |
Event JSONL, CSV journal, state store, health.json, instance lock |
dnse_auto_bot/engine/bot.py |
Multi-symbol DerivativeAutoBot + execution pipeline |
dnse_auto_bot/sdk/trading.py |
DerivativeTrading facade (symbol="VN30F1M" → contract auto) |
dnse_auto_bot/configs/default.json |
Versioned engine config (mode, symbols, strategy, alerts) |
Config + registry (easy strategy swap):
from dnse_auto_bot import DerivativeAutoBot, load_config, register
from dnse_auto_bot.strategy import Strategy, StrategyContext, TradeSignal, ACTION_BUY
@register("my_strategy")
def build_my(**params):
class MyStrategy(Strategy):
name = "my_strategy"
def evaluate(self, ctx: StrategyContext) -> TradeSignal:
if ctx.last_price and ctx.last_price > 0:
return TradeSignal(ACTION_BUY, reason="demo", quantity=1)
return TradeSignal.hold("no_price")
return MyStrategy()
cfg = load_config(overrides={
"mode": "paper",
"symbols": [{"symbol": "VN30F1M", "resolution": "5"}],
"strategy": {"name": "my_strategy", "params": {}},
"risk_profile": "conservative",
})
DerivativeAutoBot(cfg).run()Paper / backtest parity (same evaluate() as live — guide):
from dnse_auto_bot import create_strategy, run_backtest
result = run_backtest(
create_strategy("ema", {"fast": 9, "slow": 21}),
symbol="VN30F1M",
contract="41I1G8000",
ohlc=ohlc_dict, # keys t/o/h/l/c/v
)Ops: Telegram/Discord alerts (TELEGRAM_* / DISCORD_WEBHOOK_URL; inbound /status /kill /pause /switch), live metrics in health.json, trading-token refresh via TokenManager (email OTP only), exchange TP/SL via nested pnl-configs when use_exchange_tp_sl=true, restore_session + OHLC persist, soft-fail get_orders/get_positions in snapshot/reconcile. Switch strategy live via runtime/SWITCH when flat.
Margin note: VN30 initial margin ≈ price × 100_000 × initialRate (~35–40M VND / lot). Product leverage ≈ 1/initialRate (~5.4×); depositing cash raises qmax, not product leverage.
Source: dnse_auto_bot/risk.py — RiskProfile, PortfolioRiskConfig, PortfolioRiskManager.
Portfolio risk is the bot’s pre-trade + kill-switch layer. It is not the same as:
| Layer | Module | Role |
|---|---|---|
| Portfolio risk | risk.py |
Daily loss / drawdown pause, max positions, spread gate, qty sizing, opposite-signal flatten |
| Order risk (SDK) | OrderRiskConfig on DerivativeTrading |
Per-order require_affordable, qmax, min remainSecure before place |
| Position risk | engine/position_risk.py |
Per-deal SL/TP / trail / time-stop after fill (or deferred when use_exchange_tp_sl) |
| Market gates | engine/market_gates.py |
Allowlist, session, tradable, quote age (before strategy) |
check_kill(...)— each eval loop: pause new entries (and optionally flatten viaflatten_on_kill) when daily loss, drawdown, max open positions, or min remainSecure trips.check_pretrade(...)— before OMS place: qty cap, one-position-per-symbol, max spread, price deviation vs last.size_quantity(...)— clamp requested qty to[0, max_quantity_per_order]usingdefault_quantitywhen unset.
Day PnL for kill can be:
- Secure mark (default):
remainSecure − day_start_secure, or - Position PnL:
realized + unrealizedwhenuse_position_pnl_for_kill=true.
Config:
"risk_profile": "conservative",
"risk_overrides": {
"default_quantity": 1,
"max_loss_per_trade_points": 8,
"daily_loss_limit_pct": 2.0,
"use_position_pnl_for_kill": true
}Env: BOT_RISK_PROFILE=conservative (overrides JSON profile name). Overrides still come from JSON risk_overrides (+ CLI BOT_QTY / BOT_MAX_QTY / BOT_REQUIRE_AFFORDABLE in examples).
Defaults from PortfolioRiskConfig.from_profile(...):
| Profile | Intent | default / max qty | max open pos | daily loss % | max DD % | max spread (pts) | Notable flags |
|---|---|---|---|---|---|---|---|
conservative |
Live / capital preservation | 1 / 1 | 1 | 2% | 5% | 1.0 | closed_bar_only=true, no add-to-position |
balanced |
Default paper / general | 1 / 2 | 2 | 5% | 10% | 2.0 | closed_bar_only=true |
aggressive |
Higher throughput / research | 1 / 5 | 5 | 10% | 20% | 5.0 | allow_add_to_position=true, closed_bar_only=false |
fixed_qty |
Minimal gates, fixed size | 1 / 1 | 1 | off (0) | off (0) | off (0) | Still require_affordable=true, closed-bar, flatten opposite |
custom |
Blank slate | class defaults (qty 1, kill limits 0=off, …) | same | off | off | off | Fill everything via risk_overrides |
Safest preset for live money. One lot, one open position, tight spread filter, closed-bar entries only, low daily loss / drawdown pause. Used by configs/live_conservative.json and SMC live/paper configs (often with extra risk_overrides).
Default engine profile (configs/default.json, paper.json). Allows up to 2 lots / 2 open positions and looser daily loss than conservative — still closed-bar and affordable-gated.
Wider limits for more concurrent risk and optional scale-in (allow_add_to_position). Can evaluate intrabar (closed_bar_only=false) — higher noise / more signals; only use if you understand the side effects.
“Just trade this size” with kill % / drawdown / spread disabled (0). Still blocks unaffordable places when require_affordable=true, keeps one-position / closed-bar / flatten-on-opposite. Good for smoke or controlled size experiments; add kill limits via risk_overrides if needed.
Starts from dataclass defaults (most kill switches off). You must set risk_overrides explicitly — otherwise almost no portfolio kill protection.
| Group | Fields | Meaning |
|---|---|---|
| Sizing | default_quantity, max_quantity_per_order, max_open_positions, one_position_per_symbol, allow_add_to_position |
How large / how many deals |
| Margin | require_affordable, min_remain_secure, margin_buffer |
Affordability (also mirrored into SDK OrderRiskConfig) |
| Kill-switch | daily_loss_limit_pct, daily_loss_limit_vnd, daily_realized_loss_limit_vnd, max_drawdown_pct, pause_on_kill, use_position_pnl_for_kill |
Pause entries when day/DD trips |
| Market quality | max_spread_points, max_price_deviation_points |
Reject entry if BBO too wide / signal far from last |
| Execution policy | closed_bar_only, flatten_on_opposite_signal |
Bar gate + reverse signal → close |
| Position brackets* | max_loss_per_trade_points, max_hold_seconds, trailing_stop_points, point_value_vnd |
Fed into PositionRisk / kill PnL conversion (not exchange pnl-configs themselves) |
*Hard SL/TP from the strategy still prefer exchange set_tp_sl_position when use_exchange_tp_sl=true; portfolio fields above complement that.
from dnse_auto_bot import PortfolioRiskConfig, RiskProfile, DerivativeAutoBot, load_config
risk = PortfolioRiskConfig.from_profile(
RiskProfile.CONSERVATIVE,
default_quantity=1,
daily_loss_limit_pct=1.5,
use_position_pnl_for_kill=True,
)
cfg = load_config("dnse_auto_bot/configs/live_conservative.json")
DerivativeAutoBot(cfg, portfolio_risk=risk).run()Vietnamese walkthrough: dnse_auto_bot_tutorial.md § Portfolio risk.
DNSE docs: symbols uppercase; WS max 8 hours; server PING ~3 min, client must PONG within 1 min; client may send PONG proactively.
Implemented in dnse_auto_bot.sdk.hardened_ws.HardenedTradingClient (extends official dnse_sdk TradingClient):
- Proactive application
{"action":"pong"}every ~90s - Respond to server
pingwithpong - Soft-rotate session before 8h (
max_session_seconds=7h) max_retries <= 0→ unlimited reconnect + re-auth + re-subscribe- Symbols forced uppercase on subscribe
- Bot watchdog restarts unhealthy sockets
Runtime artifacts: dnse_auto_bot/runtime/{events.jsonl,journal.csv,state.json,health.json,oms_active.json,ohlc/,KILL,SWITCH,bot.lock}.
| Folder | Contents |
|---|---|
trading-api/ |
accounts, balances, PPSE, orders, OTP, post/put/cancel, positions, … |
marketdata-api/ |
instruments, secdef, trades, quotes, OHLC, sessions, … |
websocket-marketdata/ |
trade, quote, ohlc, index, foreign, session, … |
websocket-trading/ |
order/position (incl. broker) |
broker-api/ |
care-by list (broker role) |
- Market data: display (
22.55) - Orders / PPSE: VND (
22550) - Always use
normalize_order_price/place_stock_order
- Prices: index points (e.g.
1915.5) - Tick 0.1, lot 1
- Prefer
DerivativeTradingsoVN30F1Mauto-maps to41I1G8000for orders - Using
VN30F1Mdirectly inpost_ordercan returnSTOCK_PRICE_UNDEFINED - Insufficient margin → order accepted then
Rejected/PURCHASING_POWER_NOT_ENOUGH - Exchange TP/SL: nested
takeProfit/stopLossobjects; absolute prices convert to positivedeltaPricefromaveragePrice+ side - Soft-fail: prefer
get_orders(raise_on_error=False)/get_positions(raise_on_error=False)inside long-running bots
| Code | Meaning |
|---|---|
INVALID_TRADING_TOKEN |
Missing / bad token |
INVALID_OTP / OA-301 |
Bad or expired OTP |
QMAX_EXCEED / PURCHASING_POWER_NOT_ENOUGH |
Not enough buying power |
CAN_NOT_PLACE_ORDER_ON_HALTED_SYMBOL |
Symbol halted |
EDIT_ORDER_INVALID_SESSION |
Modify outside continuous session |
UNSUPPORTED_MARKET_TYPE |
close_position on STOCK |
BROKER-API-004 |
Broker API without broker role |
STOCK_PRICE_UNDEFINED |
Wrong derivative symbol form on order |
- Python 3.8+
pip install openapi-sdk
pip install --upgrade openapi-sdkpip install -r requirements.txtRun from repo root so dnse_auto_bot and dnse_sdk import cleanly (from dnse_sdk.dnse import DNSEClient).
Prefer V2 for apps. Raw official client:
from dnse_sdk.dnse import DNSEClient
client = DNSEClient(
api_key="your_api_key",
api_secret="your_api_secret",
base_url="https://openapi.dnse.com.vn",
api_version="2026-07-23",
)
status, body = client.get_accounts(dry_run=False)
print(status, body)The SDK sends the API version in the version header. If api_version is omitted, it defaults to the package default; override with DNSE_API_VERSION if needed.
| Script | Description |
|---|---|
get_accounts.py |
Sub-accounts under the API key |
get_balances.py |
Asset balances |
get_loan_packages.py |
Loan packages (needed to place) |
get_ppse.py |
Buying / selling power |
get_orders.py |
Intraday order book |
get_order_detail.py |
Order by id |
get_order_history.py |
Historical orders |
get_corporate_action_history.py |
Corporate actions |
get_execution_detail.py |
Executions |
get_positions.py |
Positions |
get_position_by_id.py |
Position by id |
close_position.py |
Close position (derivative) |
send_email_otp.py |
Request email OTP |
create_trading_token.py |
Create trading token |
post_order.py |
Place order |
cancel_order.py |
Cancel order |
put_order.py |
Modify order |
| Script | Description |
|---|---|
get_security_definition.py |
Secdef / bands |
get_instruments.py |
Instrument list |
get_trades.py |
Historical trades |
get_latest_trade.py |
Latest trade |
get_quotes.py / get_latest_quote.py |
Quotes |
get_ohlc.py |
OHLC history |
get_close_price.py |
Close price |
get_working_dates.py |
Working dates |
get_trading_session.py / get_lastest_session.py |
Session |
get_foreign_trading.py |
Foreign trading |
| Script | Description |
|---|---|
sec_def.py |
Security definition stream |
quote.py |
BBO |
trade.py |
Ticks |
trade_extra.py |
Ticks + aggregates |
ohlc.py / ohlc_closed.py |
Candles |
expected_price.py |
ATO/ATC expected |
foreign_investor.py |
Foreign flow |
market_index.py / estimated_market_index.py |
Index |
session.py |
Session events |
Set dry_run=True on REST methods to preview the signed request without sending it:
client.get_accounts(dry_run=True)- dnse_auto_bot/strategies/README.md — write a new strategy (same
evaluatefor backtest and live) - dnse_auto_system_engine.md — engine after P0–P3 (RiskGateway, restore, SWITCH, deploy)
- dnse_api.md / dnse_api.vi.md — DNSEClientV2 feature map + method catalog (current codebase)
- dnse-documents-api.md — live-verified request/response samples, error codes, stock/derivative flows
Verified against live account (derivativeAccount: ACTIVE); changelog surface includes 2026-07-23 / 2026-08-06 (paginated orders, string order ids, expected-price, nested pnl-configs, bond/egg balances).