Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
fc7e079
Removed auth for local backtest
zzk1st Mar 16, 2026
00edea4
set main ins to usual
snowboy3 Mar 19, 2026
4cc434c
Merge pull request #1 from snowboy3/master
zzk1st Mar 19, 2026
9de893e
dd
snowboy3 Mar 19, 2026
7a0d7aa
dd
snowboy3 Mar 19, 2026
8aecc8a
Performance optimization
zzk1st Mar 20, 2026
d50c86a
Merge pull request #2 from graceim-ai/zhongkai/performance
zzk1st Mar 22, 2026
5ac77df
Optimize _merge_diff, Entity, and orders property
zzk1st Mar 23, 2026
a3d6c4b
Optimize _notify_update and _simple_merge_diff_and_collect_paths
zzk1st Mar 23, 2026
a90c3c5
Cache orders_entity reference in Position.orders property
zzk1st Mar 23, 2026
f02e640
Replace isinstance with type() is checks in merge_diff functions
zzk1st Mar 23, 2026
1cdde61
Optimize orders/trade_records: replace dict.get() with direct [] access
zzk1st Mar 23, 2026
54343d2
Optimize datetime parsing: replace strptime/strftime with direct cons…
zzk1st Mar 23, 2026
cd867ef
Avoid nano->str->nano round-trip in backtest trading time check
zzk1st Mar 23, 2026
30879ec
Optimize _notify_update and skip channel logging when disabled
zzk1st Mar 23, 2026
96a43a9
Micro-optimizations: positional args, type checks, direct dict access
zzk1st Mar 23, 2026
1ca6ffe
Inline _get_obj_single into _merge_diff, optimize _gen_diff_obj
zzk1st Mar 23, 2026
acad4e7
Fix orders caching: compute once per priority group in calling loop
zzk1st Mar 23, 2026
aa14783
Remove unnecessary tuple() in _simple_merge_diff functions
zzk1st Mar 23, 2026
b4e9e58
Batch trade diff merges by prototype type
zzk1st Mar 23, 2026
5abef05
Bypass Entity method dispatch for prototype lookups in _merge_diff
zzk1st Mar 23, 2026
5fcb8e4
Optimize datetime_state.update_state and trade_base._append_to_diffs
zzk1st Mar 23, 2026
1640a07
Skip WeakSet iteration for empty listeners in _notify_update
zzk1st Mar 23, 2026
55421a5
Cache Position.orders with dirty-flag invalidation via _listener
zzk1st Mar 23, 2026
431dcc6
Build reverse index for _get_trade_price order_id lookups
zzk1st Mar 23, 2026
d35766b
Cache computed values in _update_serial_single and _process_serial_ex…
zzk1st Mar 23, 2026
d8a4a8a
Lazy WeakSet creation for Entity._listener
zzk1st Mar 23, 2026
ecaaca4
Cache time period parsing and fast-path _append_to_diffs for 2-elemen…
zzk1st Mar 23, 2026
478e36f
Bypass _is_obj_changing for serial updates with cached paths
zzk1st Mar 23, 2026
f0d6950
Inline _notify_update in _merge_diff and skip tuple(diff) when reduce…
zzk1st Mar 23, 2026
d0e87db
Add reverse index for trade_records to avoid O(n) scan per order
zzk1st Mar 23, 2026
befcd55
Add __slots__ to Entity for faster attribute access on hot paths
zzk1st Mar 24, 2026
a3d36e7
Optimize Entity.__copy__ to use getattr instead of try/except for slots
zzk1st Mar 24, 2026
0d383b0
Dispatch to _simple_merge_diff when prototype is None, skip _process_…
zzk1st Mar 24, 2026
991c502
Skip redundant _run_until_idle call when no serial extra arrays need …
zzk1st Mar 24, 2026
dec7cc7
Cache data sub-entity in _update_serial_single to avoid repeated 2-le…
zzk1st Mar 24, 2026
fdae941
Cache account_key lookups and is_stock_type results in TqMultiAccount
zzk1st Mar 24, 2026
eedb927
Skip expensive set(columns) computation in _process_serial_extra_arra…
zzk1st Mar 24, 2026
36a43f4
Bypass Entity.__getitem__ in _update_serial_single inner loop
zzk1st Mar 24, 2026
a27983a
Skip stock/futures trade split when no stock accounts are configured
zzk1st Mar 24, 2026
bdd48eb
Combine position and account diffs into single diff in _on_update_quotes
zzk1st Mar 24, 2026
8573bcf
Cache _get_trading_timestamp_nano results by (trading_time_id, tradin…
zzk1st Mar 24, 2026
b6cd274
Shared orders index + islice for trade index skip (664s -> 572s, -14%)
zzk1st Mar 25, 2026
a7bc9ac
Incremental alive-tracking orders index (592s -> 512s, -13%)
zzk1st Mar 25, 2026
37cc35b
Cache running sums in _get_trade_price reverse index (512s -> 511s)
zzk1st Mar 25, 2026
0f58105
Optimize last_only channel drain with direct deque clear (511s -> 507s)
zzk1st Mar 25, 2026
f510af0
Use mutable list for path in _simple_merge_diff_and_collect_paths (50…
zzk1st Mar 25, 2026
c524003
Revert "Use mutable list for path in _simple_merge_diff_and_collect_p…
zzk1st Mar 25, 2026
cf574b3
Inline trade price computation in _generate_ext_diff (507s -> 505s)
zzk1st Mar 25, 2026
7d91d9e
Add dirty-flag cache to trade_records index (505s -> 500s)
zzk1st Mar 25, 2026
71a05e2
Optimize sim layer: inline _get_future_margin, cache option check, sk…
zzk1st Mar 25, 2026
df96813
Cache orders and trade_records property refs in tuple for faster hot …
zzk1st Mar 25, 2026
272c6e9
Fix Entity-to-dict normalization in update_quotes to use C-level dict…
zzk1st Mar 25, 2026
cae2122
Added harness engineering code
zzk1st Mar 26, 2026
af44b6a
Merge pull request #3 from graceim-ai/auto_perf_optimization/mar23
zzk1st Apr 1, 2026
b1ae615
revert main connection contract
snowboy3 Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions harness_engineering/backtest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试脚本:使用本地行情服务器进行回测
"""

import time
from datetime import date
import logging
# 必须导入 TargetPosTask
from tqsdk import TqApi, TqAuth, TqBacktest, TqSim, TargetPosTask
from tqsdk.exceptions import BacktestFinished

LOCAL_INS_URL = "http://localhost:17788/symbols/latest.json"
LOCAL_MD_URL = "ws://localhost:17789"

logging.getLogger("TQSIM").setLevel(logging.WARNING)

def run_local_backtest():
total_start = time.time()

try:
account=TqSim()
api = TqApi(
account=account,
#backtest=TqBacktest(start_dt=date(2025, 1, 1), end_dt=date(2025, 1, 31)),
backtest=TqBacktest(start_dt=date(2025, 1, 1), end_dt=date(2025, 12, 31)),
auth=None,
#auth=TqAuth("suolong33", "suolong33"),
_ins_url=LOCAL_INS_URL,
_md_url=LOCAL_MD_URL,
disable_print=True,
)
#api = TqApi(
# account=TqSim(),
# backtest=TqBacktest(start_dt=date(2025, 3, 5), end_dt=date(2025, 3, 8)),
# auth=TqAuth("suolong33", "suolong33"),
# #_ins_url=LOCAL_INS_URL,
# #_md_url=LOCAL_MD_URL
#)

init_time = time.time() - total_start

#symbol = "SHFE.rb2505"
symbol1 = "KQ.m@SHFE.rb"
symbol2 = "KQ.m@SHFE.au"

# 尝试获取 K 线
# 注意:在本地回测中,有时需要先 wait_update 一次让图表建立
klines1 = api.get_kline_serial(symbol1, 60)
klines2 = api.get_kline_serial(symbol2, 60)

#if len(klines) > 0:
# print(klines.tail(3))

target_pos1 = TargetPosTask(api, symbol1)
target_pos2 = TargetPosTask(api, symbol2)

loop_count = 0
while True:
api.wait_update()
loop_count += 1

# 简单的退出条件,防止死循环,实际由 BacktestFinished 异常退出
#if loop_count > 100000:
# break

if api.is_changing(klines1):
if len(klines1) >= 15:
last_close = klines1.close.iloc[-1]
ma = sum(klines1.close.iloc[-15:]) / 15
current_price = klines1.close.iloc[-1]

# if loop_count % 500 == 0:
# print(f" ... 已处理 {loop_count} 次更新,最新价: {current_price}, MA: {ma:.2f}")

if current_price > ma:
target_pos1.set_target_volume(5)
elif current_price < ma:
target_pos1.set_target_volume(0)

if api.is_changing(klines2):
if len(klines2) >= 15:
last_close = klines2.close.iloc[-1]
ma = sum(klines2.close.iloc[-15:]) / 15
current_price = klines2.close.iloc[-1]

if current_price > ma:
target_pos2.set_target_volume(5)
elif current_price < ma:
target_pos2.set_target_volume(0)

except BacktestFinished:
total_time = time.time() - total_start
print(f"\n✅ 回测正常结束!")
print(f"")
print(f"⏱️ 初始化时间: {init_time:.2f}s 回测时间: {total_time - init_time:.2f}s 总耗时: {total_time:.2f}s 循环次数: {loop_count}")
# 打印最终账户情况
print(api.get_account())

except Exception as e:
print(f"\n❌ 发生错误: {e}")
import traceback
traceback.print_exc()

finally:
try:
api.close()
except:
pass

if __name__ == "__main__":
run_local_backtest()
98 changes: 98 additions & 0 deletions harness_engineering/backtest_single.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试脚本:使用本地行情服务器进行回测
"""

import time
from datetime import date
import logging
# 必须导入 TargetPosTask
from tqsdk import TqApi, TqAuth, TqBacktest, TqSim, TargetPosTask
from tqsdk.exceptions import BacktestFinished

LOCAL_INS_URL = "http://localhost:17788/symbols/latest.json"
LOCAL_MD_URL = "ws://localhost:17789"

logging.getLogger("TQSIM").setLevel(logging.WARNING)

def run_local_backtest():
total_start = time.time()

try:
account=TqSim()
api = TqApi(
account=account,
backtest=TqBacktest(start_dt=date(2025, 1, 1), end_dt=date(2025, 12, 31)),
auth=None,
#auth=TqAuth("suolong33", "suolong33"),
_ins_url=LOCAL_INS_URL,
_md_url=LOCAL_MD_URL,
disable_print=True,
)
#api = TqApi(
# account=TqSim(),
# backtest=TqBacktest(start_dt=date(2025, 3, 5), end_dt=date(2025, 3, 8)),
# auth=TqAuth("suolong33", "suolong33"),
# #_ins_url=LOCAL_INS_URL,
# #_md_url=LOCAL_MD_URL
#)

init_time = time.time() - total_start

#symbol = "SHFE.rb2505"
symbol1 = "KQ.m@SHFE.rb"

# 尝试获取 K 线
# 注意:在本地回测中,有时需要先 wait_update 一次让图表建立
klines1 = api.get_kline_serial(symbol1, 60)

#if len(klines) > 0:
# print(klines.tail(3))

target_pos1 = TargetPosTask(api, symbol1)

loop_count = 0
while True:
api.wait_update()
loop_count += 1

# 简单的退出条件,防止死循环,实际由 BacktestFinished 异常退出
#if loop_count > 100000:
# break

if api.is_changing(klines1):
if len(klines1) >= 15:
last_close = klines1.close.iloc[-1]
ma = sum(klines1.close.iloc[-15:]) / 15
current_price = klines1.close.iloc[-1]

if loop_count % 500 == 0:
print(f" ... 已处理 {loop_count} 次更新,最新价: {current_price}, MA: {ma:.2f}")

if current_price > ma:
target_pos1.set_target_volume(5)
elif current_price < ma:
target_pos1.set_target_volume(0)

except BacktestFinished:
total_time = time.time() - total_start
print(f"\n✅ 回测正常结束!")
print(f"")
print(f"⏱️ 初始化时间: {init_time:.2f}s 回测时间: {total_time - init_time:.2f}s 总耗时: {total_time:.2f}s 循环次数: {loop_count}")
# 打印最终账户情况
print(api.get_account())

except Exception as e:
print(f"\n❌ 发生错误: {e}")
import traceback
traceback.print_exc()

finally:
try:
api.close()
except:
pass

if __name__ == "__main__":
run_local_backtest()
53 changes: 53 additions & 0 deletions harness_engineering/program.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Automatic Performance Optimization

This is an experiment to have claude code to keep optimizing the performance of the framework tqsdk-python (current repo) based on cProfile results.

## Plan
You are given the source code of tqsdk-python, a backtest framework for Chinese CTA futures market, and a script to run backtest of a strategy by calling the sdk. You need to keep analyzing the profile data from cProfile, and find ways to improve the system performance.

## Setup
You are working in two repos:
1. `./tqsdk`: contains tqsdk-python's complete source code.
2. `./harness_engineering`, which has its own virtual environment and contains backtest code using the current repo (it's `tqsdk-python` in its uv environment is pointing to the current repo)

To set up a new experiment, work with the user to:
**Start by running the first profiling**. run `cd harness_engineering && uv run python -m cProfile -o result.prof backtest.py`

Once you get confirmation, kick off the experimentation.

## Experimentation

### Step 1: Analyze the existing cProfile result and find ways to improve the repo's performance.
Look at the git state: the current branch/commit we're on.

Then run this command to get the backtest's top 10 functions sorted by tottime: `cd harness_engineering && uv run python -c "import pstats; p = pstats.Stats('result.prof'); p.sort_stats('tottime').print_stats(10)"`

Then optimize the current repo's code based on the cProfile result.

**What you CAN do:**
Analyze and modify the code in `./tqsdk` however you want.

**What you CANNOT do:**
- Modify anything in `./harness_engineering`

**The goal is simple: get the lowest total backtest time, while keeping its backtest result exactly the same.**

### Step 2. Verify the optimization
Run the following command to profile the result:
`cd harness_engineering && uv run python -m cProfile -o new_result.prof backtest.py`

Two things to verify:
1. The result backtest time is shorter than previous.
2. The following backtest metrics MUST BE exactly the same as the backtest result(other metrics we don't care):
```
{'currency': 'CNY', 'pre_balance': 9165485.597521082, 'static_balance': 9165485.597521082, 'balance': 9165485.597521082, 'available': 9165485.097521082, 'ctp_balance': nan, 'ctp_available': nan, 'float_profit': 33099.99999999999, 'position_profit': 0.0, 'close_profit': 0.0, 'frozen_margin': 0.0, 'margin': 0.5, 'frozen_commission': 0.0, 'commission': 0.0, 'frozen_premium': 0.0, 'premium': 0.0, 'deposit': 0.0, 'withdraw': 0.0, 'risk_ratio': 5.455248330052814e-08, 'market_value': 0.0, '_tqsdk_stat': <tqsdk.entity.Entity object at 0x7d789e3fc9b0>, D({'start_date': '2025-01-01', 'end_date': '2025-12-31', 'init_balance': np.float64(10000000.0), 'balance': np.float64(9165485.597521082), 'start_balance': np.float64(10000000.0), 'end_balance': np.float64(9165485.597521082), 'ror': np.float64(-0.08345144024789186), 'annual_yield': np.float64(-0.08541331095548088), 'trading_days': 244, 'cum_profit_days': np.int64(92), 'cum_loss_days': np.int64(151), 'max_drawdown': np.float64(0.10702989218181808), 'commission': np.float64(14.40250000000001), 'open_times': 14403, 'close_times': 14402, 'daily_risk_ratio': np.float64(5.674929510006093e-08), 'max_cont_profit_days': np.int64(6), 'max_cont_loss_days': np.int64(11), 'sharpe_ratio': np.float64(-1.9706534357067889), 'calmar_ratio': np.float64(-0.0663809545753567), 'sortino_ratio': np.float64(-1.6926465451120536), 'tqsdk_punchline': '不要灰心,少侠重新来过', 'profit_volumes': 11850, 'loss_volumes': 60160, 'profit_value': np.float64(8928950.000000276), 'loss_value': np.float64(-9796549.999999123), 'winning_rate': 0.16456047771142898, 'profit_loss_ratio': np.float64(4.62718335334115)})}
```

If both are true,
1. use the new cProfile result for the next round and run: `cd harness_engineering && mv new_result.prof result.prof`
2. git commit with the improvement and the reduced time.

If either one is not true, then abandon the current change and start over from step 1.

### LOOP FOREVER
The idea is that you are a completely autonomous performance engineer trying things out. If they work, keep. If they don't, discard. And you're advancing the branch so that you can iterate.
21 changes: 21 additions & 0 deletions harness_engineering/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[project]
name = "tq-local-server"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
"ipdb>=0.13.13",
"orjson>=3.11.7",
"snakeviz>=2.2.2",
"tqdm>=4.67.3",
"tqsdk",
]

[[index]]
name = "tuna"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true

[tool.uv.sources]
tqsdk = { path = "../tqsdk-python", editable = true }
6 changes: 6 additions & 0 deletions harness_engineering/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# How to run it?
1. Launch claude (You may want to use `claude --dangerously-skip-permissions` to skip permissions)
2. in claude, type "Hi take a look at harness_engineering/program.md and let's start our first experiment."

# What's run_benchamrks.sh for?
It's used to re-profile all claude commits
48 changes: 48 additions & 0 deletions harness_engineering/run_benchmarks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash
# Benchmark each commit ahead of origin/master
# Runs backtest.py for each commit and saves output to a log file

set -e

REPO_DIR="/home/zzk/Projects/tqsdk-python"
TEST_DIR="/home/zzk/Projects/tq_sdk_test"
CURRENT_BRANCH=$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD)

# Get commits in chronological order (oldest first)
mapfile -t COMMITS < <(git -C "$REPO_DIR" log --reverse --format="%H %s" origin/master..HEAD)

TOTAL=${#COMMITS[@]}
echo "Found $TOTAL commits to benchmark"
echo "Current branch: $CURRENT_BRANCH"
echo ""

COUNT=0
for entry in "${COMMITS[@]}"; do
HASH="${entry%% *}"
MESSAGE="${entry#* }"
SHORT_HASH="${HASH:0:7}"
COUNT=$((COUNT + 1))

# Sanitize message: first 50 chars, replace non-alphanumeric with _
SAFE_MSG=$(echo "$MESSAGE" | head -c 50 | sed 's/[^a-zA-Z0-9_-]/_/g')
LOGFILE="${TEST_DIR}/${COUNT}_${SAFE_MSG}_${SHORT_HASH}.log"

echo "[$COUNT/$TOTAL] $SHORT_HASH $MESSAGE"

# Checkout the commit
git -C "$REPO_DIR" checkout --quiet "$HASH"

# Run backtest and save output
echo "Commit: $SHORT_HASH $MESSAGE" > "$LOGFILE"
echo "Date: $(git -C "$REPO_DIR" log -1 --format='%ci' "$HASH")" >> "$LOGFILE"
echo "---" >> "$LOGFILE"
(cd "$TEST_DIR" && uv run backtest.py) >> "$LOGFILE" 2>&1 || true

echo " -> saved to $(basename "$LOGFILE")"
echo ""
done

# Restore original branch
echo "Restoring branch: $CURRENT_BRANCH"
git -C "$REPO_DIR" checkout --quiet "$CURRENT_BRANCH"
echo "Done! All $TOTAL benchmarks complete."
Loading