From adba888bb2cf694496755cf80fe9dd40b4b72cad Mon Sep 17 00:00:00 2001 From: blade <8019068@qq.com> Date: Wed, 17 Sep 2025 19:33:37 +0800 Subject: [PATCH] 1. optimize binance ma quant strategy 2. support monitor 1m volume --- config.py | 22 + .../huge_volume_chart.cpython-312.pyc | Bin 23613 -> 23644 bytes .../db/__pycache__/db_manager.cpython-312.pyc | Bin 11888 -> 11919 bytes .../ma_break_statistics.cpython-312.pyc | Bin 35643 -> 40337 bytes core/trade/ma_break_statistics.py | 650 ++++++++++++------ market_monitor_main.py | 24 +- trade_ma_strategy_main.py | 58 +- 7 files changed, 515 insertions(+), 239 deletions(-) diff --git a/config.py b/config.py index cec1796..561d67b 100644 --- a/config.py +++ b/config.py @@ -71,6 +71,28 @@ OKX_MONITOR_CONFIG = { }, } +OKX_REALTIME_MONITOR_CONFIG = { + "volume_monitor": { + "symbols": [ + "XCH-USDT", + "BTC-USDT", + "SOL-USDT", + "ETH-USDT", + "DOGE-USDT", + ], + "bars": ["1m", "5m", "15m", "30m", "1H"], + "initial_date": "2025-05-15 00:00:00", + }, + "price_monitor": { + "symbols": ["XCH-USDT"], + "bats": [ + {"bar": "5m", "threshold": 0.025}, + {"bar": "15m", "threshold": 0.5}, + {"bar": "1H", "threshold": 0.1}, + ], + }, +} + BINANCE_MONITOR_CONFIG = { "volume_monitor": { "symbols": [ diff --git a/core/biz/__pycache__/huge_volume_chart.cpython-312.pyc b/core/biz/__pycache__/huge_volume_chart.cpython-312.pyc index e6231fd138d042421cb5313e3797b2327bff8fdd..29a3f92fb730736b266b2651af8a62a33cd3c4ee 100644 GIT binary patch delta 523 zcmdnHgYnJ|M&8rByj%=Gz@dIJqrBj4l-e_2UBn3**kDf~5@vl-?x*#H%ms7-Ekm6?3XUsO;a zMR1J}%w&+N$shd18HFcv1el6Lb--j98EQCdgi=H%hXgn>icg*vFqe^QvPqz~s%Q-x z&=+Dg!a(y`U^X%`L^7ms)e6@MPhjj>n8G!ANua&11Xv9drkb8Mh}v?-a)t`#NQQDo zP07syL5hsxJgSkZxvD9u9;&{o!J3S>*wXTgauZ878w7JQG9KM*6;jN|STT8Bs3zZu zxGM%R7xZH<#3o%3N#6V@)Qibo#NLCM>9DYXyBYgoWhX5l=eUfhhbhZ(Ggc1#7oE}E3CwQbibXia6vVz#AoSq8oC+s9VrP)tPvwA9QE{>>YV!Sx{bCfpY>B;iZ n){Ji_M@QRAeF6q%kr9Y60204AY;yBcN^?@}iWnzvitYvgE<}X+ delta 501 zcmcb!gK_T;M&8rByj%=Gkn;aT#?Fnr*1k-KSSBaht4_}MRhC>0k^zF*3@NO0S!Ms$;c4Nkit?b8z zJ>@n}il}E|yf9fPTAT6IWV>i<#y69zqHU!<0t2SV2t*hFiC-Kxx%nxjIjMF<43jTL GcLM(GuZD0$1@Cb;Bv4T7-)r3dlK}o=7?XFnU?v~l*pozlK zg9(_q7_XjGE@Ttq#XrD!A%=uFUbuKC9*qgkZ>qF0?BSE&%={kT@5~;rY_EpDgu_7} z`@SYWKcsu%b^+x{LeLaL+?C!VQV`~lOm>8)NFfpNN}$E?8v!G@8zM_$r`EbaW!J3M ziWC)}gtcX@su@MCpv`NyQ-~@LY+Th{&k?6l@->^WhkW&hSc57DsUGA6-#!7iq3k)k=RmYAMWvJ;J1W&$zkNq zSRZ6+fFz(FFu;%!F>jKFjSPb$GStiUws&&kB3sgyl5JUL$)$>G7ugU#6U$JOCD~~I z0PisZsS>2}P869Aq_Ae71`a#Ok=o&HN6XUqC=}-a*BDX}JOS|};5wic0K@SVfX^Bx zgR#!!46J7XHvoBtl)vr*6yP~PhCGVJE)-bk_!+o{QyE=MM%3Gj4aq8(AwWeNV2mM; zFF`mCxJpiXw9SUh^JmJ8vs5Z$-C8rLRIb>iCgJcpcp6Z@E{IMYP_bIJnrsA{Y%D?3 zpliE2d82mpaaX(4t1Sg)G+CLAonfmpU&ybQ|~fDU`o zecP_!H5R`0;RI~=ke}HPQ62`^Iu)TxO}7l)!4~;6IE5z3$zW#;Lux&rMZf&Hs%>mk O%nAIMj11)@IQav~G2<2h delta 1088 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 delta 12694 zcmb_?d01T6mFIi47ZgQN8wFIc7E1xeju2XqSY%0{9jz=v1+PG$TF|RPYmrhXZbwPW z7Rq&7D3T&Paeu*|nGPN)HW_ESaZi6eO5&M*&qz&8+3qOaUZPIAgCx82C10jz&bd_u z$g#ifKcpfB_)Z0-`SqOI&PT0qP4J9SBg8rEWq+fOrB7X#4xUm^npfYYr#hL(;5Bv`<92CXX`q*TOkQ)BIj&C!y@m1$Plh+ME7P0R zmF2Z|S-rL{n>V{Fo9dLF9B*z{F6C98Ja2wiKJWz$r}h+h?Ok@NNb(eVi@J)u#a+da zP99?2Nh5p6Nwa~~kj-pfRm!srr+rO=?O>QL$63~q`ViS&4oOtSxq|NCu-6?`Bjwzl zpzHP>Hj@oQ<(aFDo9SXXrc1)HT~f}%N!&6{>Xr@4IN7uEE;%QEmg!P(8Jxnc9F%ZM zkg1kaDoCjzrFJX1B>0au3N9I>NnB<^3Z66|Nmetle_R&Yaay-jGm?pIkf*veBUa>0 zI&h%k(!e}b&46P)XsC?=UCTHFXmqRPIWf5rH zTz}v3u_1TgDZgjT>+XZGckx&}Z$_dc?%;Aob_LOF=Dk=-OJ%jNQ$#+t=$uK|pWy(pd9hks> zfjN4CdEbj4C?&m+*_J!n(Nv{OQmS3oK0G=tiLF`lydtMT^gIU9xKTX@{L#;);Kpe zmv4qAzm^#ERqSEXruQUc^XMFyOeUJD%-i?%UuW4F(rdJ_*U5Qfo$jV0F?=ix;7A5} zJ>5WlWz2M}3uZxH`U@@FaM}}XP#k+{dq{bKy|g1F8L`nEJDQToPLo3lbA(*UFy?Z) zm1PQ3C+(?Xr14DiCQb288FT5+Wz4F=B#8>e3Y(DeHNiY0_-#aI*06a*HEJb|=2W(r zq&1ohDo!j64`MnmH3^9$Im(%YY0cPMpFS>98GSz(UUA ztekBo`!yx?#(*nVW>#1oVnb>!X9EafJ*c_dWiAgnlu%!lv0STZuqu&CG%ArFo_YVV zTs7zd&1qo@j#iw@KbH*EDL98ZMXX9-W1=o=$;;-P)NY0umx0W@B6Gt07?+0>kb_!= zWG7&?L5kXhJCi2S%Vq=mkO4HNoR9!rPRvD{_xUXAxvygyg^D&ae%qV}fZ z)jg~Fe9v;aK?yCr2}@7W($Z5ysfp5B|Gm=kVlA0qFSW@Sphuxa>LG?J`cEwNrT*gC zUQ7#hlGhtyA*@XGlGhZ_eE7wtx?Bgn>LAsb7BU}JC#3~jV_Jrs44TVIxw7EaxPS?z zB_=~+@r|cJ=5kujcAPp|eMoB`?&t8Qr16RiauesT#BBn z2&KW6QW??%tqQR~t8XgD4M7#TG-j2IbkYs!n^tqOA+ZOH8%fxhW2y-muQNB7w*b1~ z9iY~~d@(7-6wFIBZsZ_J(~OD2>zQ$L$TV^Ur6x01%hk=)Cn`kuoP=i0=QQf&JPn`I z=wceBnOln*@`=qx_-tbuY&OfaPKOP-H&&BSdO}KT-AAR0FS4yLK4q)!WX>`LOf_uU zrzI?N1olXlxwQX`^ayjBbu{iG|D0W+a-SJ+dn);Y+y9i^$C6{Y{pN$v6;Vatthe9q ziAwujd{i13^OE~{g<)khd2}GyH*kEY4@4^ouj|aJ#IP@_Ntk)vF5ikO5agoT6%{uO z%9KQUz(3{-@;GN!WI=z><%w$JZ|!4o(}x@wJ%r5)Eb-lpZ|OkL?h{pk6ZK7M2R7jItr=!bty#_e`#6>lQ1*o*fg zFo{b1fvC#sI^pJq`2fETMY0p_vw;<`5>b4)DFSro0;my&p)B=X*dzfe~Mi9BkB3 z@ljXscvMFIy70Wp!XqHzJIL0eMn$aG$#X?pwje|Jutg!^n+aRe7{*!#D&Spxfr%uiE+%Ka zo17IXIxHL+6b=swL&pXC@Wr-!n!HHiA>puFI5a2>4haRvFSgy+=$zNjk~b>-8? z=j4L3X+gJXUM=d{f0kyvVu<7x#{cq)ktryPghfqpq_m83VPMP3FSZNm#iFLnrhHy6;2%% zGb&49ah0KO0Oi@bNgdzfRy<5yGU(Bi%vuYQz>Yp%$X=UV{(y?%4eS5@O_DCw( zkoiew-nHWEjwLyhQ8Jw>TGlQlF^;v1C7Z>P%?l+hj~Q9U0DEoQWb2e6VqZIF5$!GW zgQ9&0CQPDz(|oOHf98>twQpk|fG9__Z@=3q>^&y#bcuG?Be}#rz)rR;DVW{tJ{HDG z+|@6HtyPQGI?-A;yMJzzP~ZNpwPQ(*bD!vh;XNQa4+@sjNX6#) zYO$j2u2gh(KpHA(t6Wku83nhr*R&B^@w7^`RnHz2ZR;aBj_E=%r)suU%xQp%*%}{M z%a-I4oAZ%WmXQfLOI4fMMMtaXXeB$#^2xuIWgS`4Gq$q-xW97rSMdb>J#J;8SC^#F z=SzE7Ce!{{l9Hx>Bu&lKUTj~olbz1(Y$o}EbGN?c`{l2ezgDqW+ANkf&zCHec95is z{L=h2!kXPe{T^Y@e&K*e*zXm*KEWRneB(M)bOrZxJrP|2>8s2zr>Ii&OGYL$dog33n6Ykd-Te4M#sM->X;lez z+wM9SG7dw+XgAmdjqQOU{Wp1-fdTTn0G`qAS)0=iG15gwen@@$N#*Y(Y!>;Kn!ZhQ zbkBifmxuo$unr~NK6!-Q{8v#Y#~qEQJ0$-cN-v^WT4HCqRQorI*Fh3UgPp{vj2)J) zlR#YwtGIom?tijH2zORq83Qs%CVtC@>0`>5Kr1Z3w_ptt-dy3x`4soZ3ubX$TJtcmj$yecTi>!D;Vn!^}8<4ZwHw z6cmyG7ensJz*#AVN5CFCqND(qPi0!*3MiKfoSkwAF$*b&c(I6bdfa-Fs(R*-8LAfzVKvHVF04j5<**t~%3(DsD2LUkq#RbGigH+uYRX|X zYAA=bsHGg%qKDZ_6|lzY#OyBwP4jk>Ch3F z)Y3x-?T>_Le@p}oiZy=RvZ`AwD_t>efe9rES-934((#N?dMIO10$5e)| zPbR;3;mwaOzr^n*`ptivydMPo0VIdWuQuC`0F3Gz;QfI>R6gMG2LP>b!-GKFE`$>U z9KR23|0@uOECwl)S4Vzc1w~ZV?>_E2H4MAP5j5^Xa*F(**`wkA8}eQxr^)V?-ts-j zo&fR?7sv1~po^lqVIROKKuvuC_kiEW1s-De^M3N{7AMG5BgkDCGYomPjAt0Y#cfpl>56Hf@ z-IaU@dF(9yJtPq%Q6%pp`CB9c5)sL}KI)Q2P;Y)ZqYT@v!6j$*w~w?)`{KG_9X&5lx$f76gi7i+izUL~I!mPI$x?uW0f{z;vT%Zd^1siRPxclcKp9aDb_W zePGUj8kjNxKBZYcO=7ZgrX9Etys`<4tK>r)<@WdXK9UmS#j!6G>Y}6J| zm=pjU9vB|-xjcU`jF^)S_Xo)RJrW$$hj)M&`2PiTObz7YKb`&f`9J^lkG@TAcIEWp zp^>MB;{0$(c)DV#^biURtL7cHh=x~3@!%E<&^BOxTwL@3LeCgu2Va=d@Ej~g5H3=hBz55(5M@{)iPAU%;RPipL{pbJ_Jfnpl`b@L5`bnmmlftU$k zpcEpg%fA>p_zgoOJq3!_(PE60^;|EA=q;1xN!65kxhoNA53yM77H+w~reh2*xJEmsgAq9ShyWLy#dhSE4Ex9Lqh!fnZdkAjO&UBne)@SD6s|N16~z z-qxE)t@ky!ez_qb54;8GvR*)bG>|Aq+`^?~K%S8l0BknU!5%)YgqKfT9#UfHkhmHP z$VuOt4DC#6;%yC~7CEOwEpG~`$lGg8>NHM2%yM8u?)eI(+8G1+y)Q3I3zF4g&Umf~ z34fWC`*WnG88g}C- zzS7S@%^{B-TL7U;JyDG2ke`p%tg_v3(wS-t;SEjpO&OQN<<8`hGbbJLe6C=|PA-g@ z$Xh3k^1|iR!;_X|3Prghu6U+|X!yNyE$5giCH?&OT31Siuq0P@q6b z}r{_Mp|sSs>=E;?~YIE(2RVdFEt6 za;)3d-ITq?l5;1Er51QjpDZFBO&ap|C$lA75jo0R@imbgIf*=R?r{BQ)w`iWylM`of#YHEn8Da0%N!d zu1XO9zz@QfJ!3g?Be!{`8OE`ptc8wpE85+Z12#8?q#+Zwb3%5>2jO7Zr9Xjiu(XqL zw2^-pdmF~!l~awqpXtHk;ny#Jbm^r}{_{(p{OO;?!c$|GFBE@t^Xng7_z`U0!=R1@ z$GU*q4;QS;0laJT1$erpQ~d}@JDsKIMAJj0=CqwXLJpmNR#gZK_f;0cQ~!9nkKIdJ z&yQF+sYSiNoMm0fri3a;TuCxBJARk7vYe>G08r<2;3t{=l5qtoXtX~SIqJWDoezo7^ z-C@0(vWf8YA5 z^-lkrCtg1>UoamIywfV|IsDU|3v0R-9NnKnaHE%<*a8UYi(KsztKly7V8pR8;mh`_ z?GFFu`Pa|S*WQ)B1JR!&KkZ#;=w5L2psS-XS0>BV?N_!>8K&e@2ZiztLfJ-PW4Dmq zBMc3Pg;SyX#_@RDC(q30-#6ALQeT;q+&8XIq)twiOs^I5>*o0Rq5H;Naq){gklA)+ z+vNUf!+oO@I-#QBPGGKnzC`Hg7F&828hRI;M;|j%(_U7zI40R^O_Oa?Mekd(ZmF)R z!c)fYWxkyGUF)puP3`O2g{sZ-X<}82nAbA@jBv0=IOclypzFt+Fys;2ypaOObiq{l zv`fgXnmv3MVx32Ra^me0p^u9=N@q{acm1nelChUvl44n(LO^S|B7I`Zv4sZLg0mkh zvTvm#;pLkA|Ex$iR%9Rh2r9AXLrxn3C3+t z+JX?KRit|sTg zj$|o7me}nfix+HLWe}E;hoo(=@%Osmb)g8!D1Uev;Lx*pNP!S0kv9f+61TkLDgZXD z(x~9sONGF`zQ;(KE)+;1m_>fR#)#K|@+3|cN+P4)OpI&M_aIH0QkD4X0U3>9#bxTa zjK;9yvgEi-69X~e%0xD?a&$*8K;veqaWmbDS!!Bb7RNuBHPsN88CTiSw;=e7U1Vcz zO{wW|JBoE;WvDYy#z|u-^88m-EVWf@$T4Ih2X!^atN0L7A1bOn^ypR3J9w zN;n6g#?loK^QC!c*<2Y{4tbnknkO5(ssNE{MMw#>GKOS0fJ-wqD^aLm2KkF;t@`*q zYa9$=h2bj@5XUZASBv435FOV5FcIFR_@lNqvo#F;jG%AD))6y6Or0?1*5NlT{7-^q z2_3gS_=R@oz)!;0crqs)V#hT>ge>$IO8$df01_E)Jq?c~Cz=P?(E#fczET*e#O?^E zBq{v3`wZ%Fv$HgZifn-Z9sWqfN_mw}TiH8H8^bG4VOFZBZByqUMcw*oR4 z$F-#AB3$ZIfGuT4ny|}qa0YFrGpLzj=9X2MSqs^dzF{@O7agr!+su|kztcI<9xSd| z$^1+lNT-J2Gq(`kegHyuQy|R&kd_0G-a7UPLw@^>?~`AAbA9*-^i3>Ah%;&^)`?4- zVo;ez8)>}o6q=nz@()P<3CTYq`6ZGMk^BnDM@aq|$;U|k1<5B!evRZ8NS2U1MDmDX zj>Q9JPcZFA@)?qU1+t%yVQ^rhTD%UY5z)S&zpt8qj7IQzC438qBo9bb)(`NScOc99 z9df=DHDx60Qh}}jg>s6>qig_Tue&Z4cOt0(GKLWS zmvh+gboj1aINT%X%I@iUBf6YLU74sWn>Ee_si+OWR^6I6n_h34uf&Vjmi~p>fr-?q zR8dz2UhCV~`3|vekI>mI^bCoe$Hlti6R7}dMO}5Iu2pE;FV-Dctm_u*K;OGicQl~{ zw}70Iu7s1O8t&`LVoB8%)nv|ej+oInx9RRCaea3ryJoKFZnJRk2;BOACS}ux*%;(n zZHeG~eGi%z-`y)5>Vl63&~${2IaOa#Pad4yHq|^eFm*sEZJY0%-z{YBkL25D&7VmX zY+B=oIs@7Ht-Ik#=nOGu(_&7Wm;+{FPG=~=A`{ch7jb{Ea^8O?$SG6q1` zd?^C8O&p(eO-U!mrgqHQW>aBkjBU#ZlxSaEmp(4RDX}EQIkL1_n!ug6`(AuUqNFRs z!;qS*n-pKgnff(y`l{{V9+-Vm%>eI)xNBeFxZ54%>7iZi;<&zHuWJZC!QKMj6hUm) z@1sFT_*@o_wNd#f4>u-`q}Y1+6P(1C(3j+NKb&?yylT&gvhD!H_L${BZ2Y6vTQIfL z$+}6CWTTYqoqUofdFB}h%kKhHcmTe|OCC(^+7-LW zROb?d&*?zyIbZvPrXRJ-YUIYLR&Xmfq62s=`N&^>dsp}bE6ar5PZ1TVWDM|Z{ERzadsFC1=;SrJZ z8glN2l}#a6Z`fEZ`N566*w^Aov?*i3VNZb9p&bU2`Q2DN9mek>w>-4JQ*H42xiOEs ziN6C1_+tXcfhUSy~n8EV+`*x`VW|#_ZZX9m=Y>U2Gs}1r+&cXe!%4Yj5&Fq Yk)8a2sU$r&i&DR$VQU{VNNGv`5716i_W%F@ diff --git a/core/trade/ma_break_statistics.py b/core/trade/ma_break_statistics.py index 45c68a6..accba00 100644 --- a/core/trade/ma_break_statistics.py +++ b/core/trade/ma_break_statistics.py @@ -8,12 +8,19 @@ from datetime import datetime, timedelta, timezone from core.utils import get_current_date_time import re import json +import math from openpyxl import Workbook from openpyxl.drawing.image import Image import openpyxl from openpyxl.styles import Font from PIL import Image as PILImage -from config import OKX_MONITOR_CONFIG, US_STOCK_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE, BINANCE_MONITOR_CONFIG +from config import ( + OKX_MONITOR_CONFIG, + US_STOCK_MONITOR_CONFIG, + MYSQL_CONFIG, + WINDOW_SIZE, + BINANCE_MONITOR_CONFIG, +) from core.db.db_market_data import DBMarketData from core.db.db_binance_data import DBBinanceData from core.db.db_huge_volume_data import DBHugeVolumeData @@ -34,7 +41,12 @@ class MaBreakStatistics: 之间的涨跌幅 """ - def __init__(self, is_us_stock: bool = False, is_binance: bool = False): + def __init__( + self, + is_us_stock: bool = False, + is_binance: bool = False, + commission_per_share: float = 0.0008, + ): mysql_user = MYSQL_CONFIG.get("user", "xch") mysql_password = MYSQL_CONFIG.get("password", "") if not mysql_password: @@ -44,7 +56,7 @@ class MaBreakStatistics: mysql_database = MYSQL_CONFIG.get("database", "okx") self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}" - + self.db_huge_volume_data = DBHugeVolumeData(self.db_url) self.is_us_stock = is_us_stock self.is_binance = is_binance @@ -65,16 +77,16 @@ class MaBreakStatistics: "symbols", ["BTC-USDT"] ) self.bars = ["30m", "1H"] - self.initial_date = BINANCE_MONITOR_CONFIG.get("volume_monitor", {}).get( - "initial_date", "2017-08-16 00:00:00" - ) + self.initial_date = BINANCE_MONITOR_CONFIG.get( + "volume_monitor", {} + ).get("initial_date", "2017-08-16 00:00:00") self.db_market_data = DBBinanceData(self.db_url) else: self.symbols = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( - "symbols", ["XCH-USDT"] + "symbols", ["XCH-USDT"] ) self.bars = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( - "bars", ["5m", "15m", "30m", "1H"] + "bars", ["5m", "15m", "30m", "1H"] ) self.initial_date = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "initial_date", "2025-05-15 00:00:00" @@ -82,7 +94,8 @@ class MaBreakStatistics: self.db_market_data = DBMarketData(self.db_url) if len(self.initial_date) > 10: self.initial_date = self.initial_date[:10] - self.end_date = get_current_date_time() + self.end_date = get_current_date_time(format="%Y-%m-%d") + self.commission_per_share = commission_per_share self.trade_strategy_config = self.get_trade_strategy_config() self.main_strategy = self.trade_strategy_config.get("均线系统策略", None) @@ -93,14 +106,26 @@ class MaBreakStatistics: def batch_statistics(self, strategy_name: str = "全均线策略"): if self.is_us_stock: - self.stats_output_dir = f"./output/trade_sandbox/ma_strategy/us_stock/excel/{strategy_name}/" - self.stats_chart_dir = f"./output/trade_sandbox/ma_strategy/us_stock/chart/{strategy_name}/" + self.stats_output_dir = ( + f"./output/trade_sandbox/ma_strategy/us_stock/excel/{strategy_name}/" + ) + self.stats_chart_dir = ( + f"./output/trade_sandbox/ma_strategy/us_stock/chart/{strategy_name}/" + ) elif self.is_binance: - self.stats_output_dir = f"./output/trade_sandbox/ma_strategy/binance/excel/{strategy_name}/" - self.stats_chart_dir = f"./output/trade_sandbox/ma_strategy/binance/chart/{strategy_name}/" + self.stats_output_dir = ( + f"./output/trade_sandbox/ma_strategy/binance/excel/{strategy_name}/" + ) + self.stats_chart_dir = ( + f"./output/trade_sandbox/ma_strategy/binance/chart/{strategy_name}/" + ) else: - self.stats_output_dir = f"./output/trade_sandbox/ma_strategy/okx/excel/{strategy_name}/" - self.stats_chart_dir = f"./output/trade_sandbox/ma_strategy/okx/chart/{strategy_name}/" + self.stats_output_dir = ( + f"./output/trade_sandbox/ma_strategy/okx/excel/{strategy_name}/" + ) + self.stats_chart_dir = ( + f"./output/trade_sandbox/ma_strategy/okx/chart/{strategy_name}/" + ) os.makedirs(self.stats_output_dir, exist_ok=True) os.makedirs(self.stats_chart_dir, exist_ok=True) @@ -111,7 +136,7 @@ class MaBreakStatistics: for symbol in self.symbols: for bar in self.bars: logger.info( - f"开始计算{symbol} {bar}的MA突破区间涨跌幅统计, 策略: {strategy_name}" + f"开始计算{symbol} {bar}的MA突破区间涨跌幅统计, 策略: {strategy_name},交易费率:{self.commission_per_share}" ) ma_break_market_data, market_data_pct_chg = self.trade_simulate( symbol, bar, strategy_name @@ -126,40 +151,19 @@ class MaBreakStatistics: f"{symbol} {bar} 的市场价格变化, {market_data_pct_chg.get('pct_chg', 0)}%" ) market_data_pct_chg_list.append(market_data_pct_chg) - + if len(ma_break_market_data_list) > 0: ma_break_market_data = pd.concat(ma_break_market_data_list) market_data_pct_chg_df = pd.DataFrame(market_data_pct_chg_list) # 依据symbol和bar分组,统计每个symbol和bar的pct_chg的max, min, mean, std, median, count - ma_break_market_data.sort_values(by="begin_timestamp", ascending=True, inplace=True) + ma_break_market_data.sort_values( + by="begin_timestamp", ascending=True, inplace=True + ) ma_break_market_data.reset_index(drop=True, inplace=True) - pct_chg_df = ( - ma_break_market_data.groupby(["symbol", "bar"])["pct_chg"] - .agg( - pct_chg_sum="sum", - pct_chg_max="max", - pct_chg_min="min", - pct_chg_mean="mean", - pct_chg_std="std", - pct_chg_median="median", - pct_chg_count="count", - ) - .reset_index() - ) - pct_chg_df["strategy_name"] = strategy_name - pct_chg_df["pct_chg_total"] = 0 - pct_chg_df["market_pct_chg"] = 0 - # 将pct_chg_total与market_pct_chg的值类型转换为float - pct_chg_df["pct_chg_total"] = pct_chg_df["pct_chg_total"].astype(float) - pct_chg_df["market_pct_chg"] = pct_chg_df["market_pct_chg"].astype(float) - # 统计pct_chg_total - # 算法要求,ma_break_market_data,然后pct_chg/100 + 1 - ma_break_market_data["pct_chg_total"] = ( - ma_break_market_data["pct_chg"] / 100 + 1 - ) - # 遍历symbol和bar,按照end_timestamp排序,计算pct_chg_total的值,然后相乘 - for symbol in pct_chg_df["symbol"].unique(): - for bar in pct_chg_df["bar"].unique(): + + account_value_chg_list = [] + for symbol in market_data_pct_chg_df["symbol"].unique(): + for bar in market_data_pct_chg_df["bar"].unique(): symbol_bar_data = ma_break_market_data[ (ma_break_market_data["symbol"] == symbol) & (ma_break_market_data["bar"] == bar) @@ -169,51 +173,80 @@ class MaBreakStatistics: by="end_timestamp", ascending=True, inplace=True ) symbol_bar_data.reset_index(drop=True, inplace=True) - symbol_bar_data["pct_chg_total"] = symbol_bar_data[ - "pct_chg_total" - ].cumprod() - - # 将更新后的pct_chg_total数据同步更新到ma_break_market_data的对应数据行中 - for idx, row in symbol_bar_data.iterrows(): - mask = (ma_break_market_data["symbol"] == symbol) & \ - (ma_break_market_data["bar"] == bar) & \ - (ma_break_market_data["end_timestamp"] == row["end_timestamp"]) - ma_break_market_data.loc[mask, "pct_chg_total"] = row["pct_chg_total"] - - last_pct_chg_total = symbol_bar_data["pct_chg_total"].iloc[-1] - last_pct_chg_total = (last_pct_chg_total - 1) * 100 - pct_chg_df.loc[ - (pct_chg_df["symbol"] == symbol) - & (pct_chg_df["bar"] == bar), - "pct_chg_total", - ] = last_pct_chg_total + initial_capital = int(market_data_pct_chg_df.loc[ + (market_data_pct_chg_df["symbol"] == symbol) + & (market_data_pct_chg_df["bar"] == bar), + "initial_capital", + ].values[0]) + final_account_value = float(symbol_bar_data["end_account_value"].iloc[-1]) + account_value_chg = (final_account_value - initial_capital) / initial_capital * 100 + account_value_chg = round(account_value_chg, 4) market_pct_chg = market_data_pct_chg_df.loc[ (market_data_pct_chg_df["symbol"] == symbol) & (market_data_pct_chg_df["bar"] == bar), "pct_chg", ].values[0] - pct_chg_df.loc[ - (pct_chg_df["symbol"] == symbol) - & (pct_chg_df["bar"] == bar), - "market_pct_chg", - ] = market_pct_chg - - pct_chg_df = pct_chg_df[ + total_buy_commission = float(symbol_bar_data["buy_commission"].sum()) + total_sell_commission = float(symbol_bar_data["sell_commission"].sum()) + total_commission = total_buy_commission + total_sell_commission + total_commission = round(total_commission, 4) + total_buy_commission = round(total_buy_commission, 4) + total_sell_commission = round(total_sell_commission, 4) + account_value_chg_list.append({ + "strategy_name": strategy_name, + "symbol": symbol, + "bar": bar, + "total_buy_commission": total_buy_commission, + "total_sell_commission": total_sell_commission, + "total_commission": total_commission, + "initial_account_value": initial_capital, + "final_account_value": final_account_value, + "account_value_chg": account_value_chg, + "market_pct_chg": market_pct_chg, + }) + account_value_chg_df = pd.DataFrame(account_value_chg_list) + account_value_chg_df = account_value_chg_df[ [ "strategy_name", "symbol", "bar", + "total_buy_commission", + "total_sell_commission", + "total_commission", + "initial_account_value", + "final_account_value", + "account_value_chg", "market_pct_chg", - "pct_chg_total", - "pct_chg_sum", - "pct_chg_max", - "pct_chg_min", - "pct_chg_mean", - "pct_chg_std", - "pct_chg_median", - "pct_chg_count", ] ] + + account_value_statistics_df = ( + ma_break_market_data.groupby(["symbol", "bar"])["end_account_value"] + .agg( + account_value_max="max", + account_value_min="min", + account_value_mean="mean", + account_value_std="std", + account_value_median="median", + account_value_count="count", + ) + .reset_index() + ) + account_value_statistics_df["strategy_name"] = strategy_name + account_value_statistics_df = account_value_statistics_df[ + [ + "strategy_name", + "symbol", + "bar", + "account_value_max", + "account_value_min", + "account_value_mean", + "account_value_std", + "account_value_median", + "account_value_count", + ] + ] + # 依据symbol和bar分组,统计每个symbol和bar的interval_minutes的max, min, mean, std, median, count interval_minutes_df = ( ma_break_market_data.groupby(["symbol", "bar"])["interval_minutes"] @@ -227,6 +260,20 @@ class MaBreakStatistics: ) .reset_index() ) + interval_minutes_df["strategy_name"] = strategy_name + interval_minutes_df = interval_minutes_df[ + [ + "strategy_name", + "symbol", + "bar", + "interval_minutes_max", + "interval_minutes_min", + "interval_minutes_mean", + "interval_minutes_std", + "interval_minutes_median", + "interval_minutes_count", + ] + ] earliest_market_date_time = ma_break_market_data["begin_date_time"].min() earliest_market_date_time = re.sub( @@ -238,7 +285,10 @@ class MaBreakStatistics: latest_market_date_time = re.sub( r"[\:\-\s]", "", str(latest_market_date_time) ) - output_file_name = f"ma_break_stats_from_{earliest_market_date_time}_to_{latest_market_date_time}_{strategy_name}.xlsx" + if self.commission_per_share > 0: + output_file_name = f"ma_break_stats_from_{earliest_market_date_time}_to_{latest_market_date_time}_{strategy_name}_with_commission.xlsx" + else: + output_file_name = f"ma_break_stats_from_{earliest_market_date_time}_to_{latest_market_date_time}_{strategy_name}_without_commission.xlsx" output_file_path = os.path.join(self.stats_output_dir, output_file_name) logger.info(f"导出{output_file_path}") strategy_info_df = self.get_strategy_info(strategy_name) @@ -247,16 +297,21 @@ class MaBreakStatistics: ma_break_market_data.to_excel( writer, sheet_name="买卖记录明细", index=False ) - pct_chg_df.to_excel(writer, sheet_name="买卖涨跌幅统计", index=False) + account_value_chg_df.to_excel(writer, sheet_name="资产价值变化", index=False) + account_value_statistics_df.to_excel( + writer, sheet_name="买卖账户价值统计", index=False + ) interval_minutes_df.to_excel( writer, sheet_name="买卖时间间隔统计", index=False ) - chart_dict = self.draw_quant_pct_chg_bar_chart(pct_chg_df, strategy_name) + chart_dict = self.draw_quant_pct_chg_bar_chart(account_value_chg_df, strategy_name) self.output_chart_to_excel(output_file_path, chart_dict) - chart_dict = self.draw_quant_line_chart(ma_break_market_data, strategy_name) + chart_dict = self.draw_quant_line_chart( + ma_break_market_data, market_data_pct_chg_df, strategy_name + ) self.output_chart_to_excel(output_file_path, chart_dict) - return pct_chg_df + return account_value_chg_df else: return None @@ -333,6 +388,14 @@ class MaBreakStatistics: date_time_field = "date_time_us" else: date_time_field = "date_time" + close_mean = market_data["close"].mean() + self.update_initial_capital(close_mean) + logger.info( + f"成功获取{symbol}数据:{len(market_data)}根{bar}K线,开始日期={market_data[date_time_field].min()},结束日期={market_data[date_time_field].max()}" + ) + + account_value = self.initial_capital + for index, row in market_data.iterrows(): ma_cross = row["ma_cross"] timestamp = row["timestamp"] @@ -345,7 +408,6 @@ class MaBreakStatistics: macd_diff = float(row["dif"]) macd_dea = float(row["dea"]) macd = float(row["macd"]) - if ma_break_market_data_pair.get("begin_timestamp", None) is None: buy_condition = self.fit_strategy( strategy_name=strategy_name, @@ -355,6 +417,18 @@ class MaBreakStatistics: ) if buy_condition: + entry_price = close + # 计算交易股数 + shares, account_value = self.calculate_shares( + account_value, entry_price + ) + if shares == 0: + # 股数为0→不交易 + continue + + # 计算佣金 + buy_commission = shares * close * self.commission_per_share + ma_break_market_data_pair = {} ma_break_market_data_pair["symbol"] = symbol ma_break_market_data_pair["bar"] = bar @@ -368,6 +442,9 @@ class MaBreakStatistics: ma_break_market_data_pair["begin_macd_diff"] = macd_diff ma_break_market_data_pair["begin_macd_dea"] = macd_dea ma_break_market_data_pair["begin_macd"] = macd + ma_break_market_data_pair["shares"] = shares + ma_break_market_data_pair["buy_commission"] = buy_commission + ma_break_market_data_pair["begin_account_value"] = account_value continue else: sell_condition = self.fit_strategy( @@ -378,6 +455,20 @@ class MaBreakStatistics: ) if sell_condition: + shares = ma_break_market_data_pair["shares"] + entry_price = ma_break_market_data_pair["begin_close"] + exit_price = close + sell_commission = ( + shares * exit_price * self.commission_per_share + ) + profit_loss = (exit_price - entry_price) * shares + begin_account_value = ma_break_market_data_pair[ + "begin_account_value" + ] + account_value = ( + begin_account_value + profit_loss - sell_commission + ) + ma_break_market_data_pair["end_timestamp"] = timestamp ma_break_market_data_pair["end_date_time"] = date_time ma_break_market_data_pair["end_close"] = close @@ -389,11 +480,14 @@ class MaBreakStatistics: ma_break_market_data_pair["end_macd_dea"] = macd_dea ma_break_market_data_pair["end_macd"] = macd ma_break_market_data_pair["pct_chg"] = ( - close - ma_break_market_data_pair["begin_close"] - ) / ma_break_market_data_pair["begin_close"] + exit_price - entry_price + ) / entry_price ma_break_market_data_pair["pct_chg"] = round( ma_break_market_data_pair["pct_chg"] * 100, 4 ) + ma_break_market_data_pair["profit_loss"] = profit_loss + ma_break_market_data_pair["sell_commission"] = sell_commission + ma_break_market_data_pair["end_account_value"] = account_value ma_break_market_data_pair["interval_seconds"] = ( timestamp - ma_break_market_data_pair["begin_timestamp"] ) / 1000 @@ -413,7 +507,9 @@ class MaBreakStatistics: if len(ma_break_market_data_pair_list) > 0: ma_break_market_data = pd.DataFrame(ma_break_market_data_pair_list) # sort by end_timestamp - ma_break_market_data.sort_values(by="begin_timestamp", ascending=True, inplace=True) + ma_break_market_data.sort_values( + by="begin_timestamp", ascending=True, inplace=True + ) ma_break_market_data.reset_index(drop=True, inplace=True) logger.info( f"获取{symbol} {bar} 的买卖记录明细成功, 买卖次数: {len(ma_break_market_data)}" @@ -421,16 +517,85 @@ class MaBreakStatistics: # 量化期间,市场的波动率: # ma_break_market_data(最后一条数据的end_close - 第一条数据的begin_close) / 第一条数据的begin_close * 100 pct_chg = ( - (ma_break_market_data["end_close"].iloc[-1] - ma_break_market_data["begin_close"].iloc[0]) + ( + ma_break_market_data["end_close"].iloc[-1] + - ma_break_market_data["begin_close"].iloc[0] + ) / ma_break_market_data["begin_close"].iloc[0] * 100 ) pct_chg = round(pct_chg, 4) - market_data_pct_chg = {"symbol": symbol, "bar": bar, "pct_chg": pct_chg} + market_data_pct_chg = { + "symbol": symbol, + "bar": bar, + "pct_chg": pct_chg, + "initial_capital": self.initial_capital, + } return ma_break_market_data, market_data_pct_chg else: return None, None - + + def update_initial_capital(self, close_mean: float): + self.initial_capital = 25000 + if close_mean > 10000: + self.initial_capital = self.initial_capital * 10000 + elif close_mean > 5000: + self.initial_capital = self.initial_capital * 5000 + elif close_mean > 1000: + self.initial_capital = self.initial_capital * 1000 + elif close_mean > 500: + self.initial_capital = self.initial_capital * 500 + elif close_mean > 100: + self.initial_capital = self.initial_capital * 100 + else: + pass + logger.info(f"收盘价均值:{close_mean}") + logger.info(f"初始资金调整为:{self.initial_capital}") + + def calculate_shares(self, account_value, entry_price): + """ + 根据ORB公式计算交易股数 + :param account_value: 当前账户价值(美元) + :param entry_price: 交易买入价格 + :param commission_per_share: 交易佣金, 默认为0.0008 + :return: 整数股数(Shares) + """ + logger.info( + f"开始计算交易股数:账户价值={account_value},买入价格={entry_price}" + ) + try: + # 验证输入参数 + if account_value <= 0 or entry_price <= 0 or self.commission_per_share < 0: + logger.error("账户价值、买入价格或佣金不能为负或零") + return 0, account_value # 返回0股,账户价值不变 + + # 计算考虑手续费后的每单位BTC总成本 + total_cost_per_share = entry_price * (1 + self.commission_per_share) + + # 计算可购买的BTC数量(向下取整) + shares = math.floor(account_value / total_cost_per_share) + + # 计算总成本(含手续费) + total_cost = shares * total_cost_per_share + + # 计算剩余现金 + remaining_cash = account_value - total_cost + + # 计算总资产价值 = (购买的BTC数量 × 买入价格) + 剩余现金 + remaining_value = (shares * entry_price) + remaining_cash + + # 记录计算结果 + logger.info( + f"计算结果:可购买股数={shares},总成本={total_cost:.2f}美元," + f"剩余现金={remaining_cash:.2f}美元,总资产价值={remaining_value:.2f}美元" + ) + + return shares, remaining_value + + except Exception as e: + logger.error(f"计算股数或账户价值时出错:{str(e)}") + return 0, account_value # 出错时返回0股,账户价值不变 + def get_full_data(self, symbol: str, bar: str = "5m"): """ 分段获取数据,并将数据合并为完整数据 @@ -464,9 +629,7 @@ class MaBreakStatistics: current_end_date = min(start_date + timedelta(days=180), end_date) start_date_str = start_date.strftime("%Y-%m-%d") current_end_date_str = current_end_date.strftime("%Y-%m-%d") - logger.info( - f"获取{symbol}数据:{start_date_str}至{current_end_date_str}" - ) + logger.info(f"获取{symbol}数据:{start_date_str}至{current_end_date_str}") current_data = self.db_market_data.query_market_data_by_symbol_bar( symbol, bar, fields, start=start_date_str, end=current_end_date_str ) @@ -483,7 +646,6 @@ class MaBreakStatistics: data.reset_index(drop=True, inplace=True) return data - def fit_strategy( self, strategy_name: str = "全均线策略", @@ -626,103 +788,122 @@ class MaBreakStatistics: plt.rcParams["font.size"] = 11 # 设置字体大小 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题 chart_dict = {} - column_name_dict = {"pct_chg_total": "量化策略涨跌", "pct_chg_mean": "量化策略涨跌均值"} + column_name_dict = { + "account_value_chg": "量化策略涨跌", + } for column_name, column_name_text in column_name_dict.items(): for bar in data["bar"].unique(): bar_data = data[data["bar"] == bar].copy() # 一次筛选即可 if bar_data.empty: continue bar_data.rename(columns={column_name: column_name_text}, inplace=True) - if column_name == "pct_chg_total": - bar_data.rename(columns={"market_pct_chg": "市场自然涨跌"}, inplace=True) + bar_data.rename( + columns={"market_pct_chg": "市场自然涨跌"}, inplace=True + ) # 可选:按均值排序 bar_data.sort_values(by=column_name_text, ascending=False, inplace=True) bar_data.reset_index(drop=True, inplace=True) # 如果column_name_text是"量化策略涨跌",则柱状图,同时绘制量化策略涨跌与市场自然涨跌的柱状图,并绘制在同一个图表中 - if column_name == "pct_chg_total": - plt.figure(figsize=(12, 7)) - - # 设置x轴位置,为并列柱状图做准备 - x = np.arange(len(bar_data)) - width = 0.35 # 柱状图宽度 - - # 确保symbol列是字符串类型,避免matplotlib警告 - bar_data["symbol"] = bar_data["symbol"].astype(str) - - # 绘制量化策略涨跌柱状图(蓝色渐变色) - bars1 = plt.bar(x - width/2, bar_data[column_name_text], width, - label=column_name_text, - color=plt.cm.Blues(np.linspace(0.6, 0.9, len(bar_data)))) - - # 绘制市场自然涨跌柱状图(绿色渐变色) - bars2 = plt.bar(x + width/2, bar_data["市场自然涨跌"], width, - label="市场自然涨跌", - color=plt.cm.Greens(np.linspace(0.6, 0.9, len(bar_data)))) - - # 设置图表标题和标签 - plt.title(f"{bar}趋势{column_name_text}与市场自然涨跌对比(%)", fontsize=14, fontweight='bold') - plt.xlabel("Symbol", fontsize=12) - plt.ylabel("涨跌幅(%)", fontsize=12) - plt.xticks(x, bar_data['symbol'], rotation=45, ha='right') - plt.legend() - plt.grid(True, alpha=0.3) - - # 在量化策略涨跌柱状图上方添加数值标签 - for i, (bar1, value1) in enumerate(zip(bars1, bar_data[column_name_text])): - plt.text(bar1.get_x() + bar1.get_width()/2, value1 + (0.01 if value1 >= 0 else -0.01), - f'{value1:.3f}%', ha='center', va='bottom' if value1 >= 0 else 'top', - fontsize=9, fontweight='bold', color='darkblue') - - # 在市场自然涨跌柱状图上方添加数值标签 - for i, (bar2, value2) in enumerate(zip(bars2, bar_data["市场自然涨跌"])): - plt.text(bar2.get_x() + bar2.get_width()/2, value2 + (0.01 if value2 >= 0 else -0.01), - f'{value2:.3f}%', ha='center', va='bottom' if value2 >= 0 else 'top', - fontsize=9, fontweight='bold', color='darkgreen') + plt.figure(figsize=(12, 7)) - else: - plt.figure(figsize=(10, 6)) - - # 确保symbol列是字符串类型,避免matplotlib警告 - bar_data["symbol"] = bar_data["symbol"].astype(str) - - ax = sns.barplot( - x="symbol", y=column_name_text, data=bar_data, palette="Blues_d" + # 设置x轴位置,为并列柱状图做准备 + x = np.arange(len(bar_data)) + width = 0.35 # 柱状图宽度 + + # 确保symbol列是字符串类型,避免matplotlib警告 + bar_data["symbol"] = bar_data["symbol"].astype(str) + + # 绘制量化策略涨跌柱状图(蓝色渐变色) + bars1 = plt.bar( + x - width / 2, + bar_data[column_name_text], + width, + label=column_name_text, + color=plt.cm.Blues(np.linspace(0.6, 0.9, len(bar_data))), + ) + + # 绘制市场自然涨跌柱状图(绿色渐变色) + bars2 = plt.bar( + x + width / 2, + bar_data["市场自然涨跌"], + width, + label="市场自然涨跌", + color=plt.cm.Greens(np.linspace(0.6, 0.9, len(bar_data))), + ) + + # 设置图表标题和标签 + plt.title( + f"{bar}趋势{column_name_text}与市场自然涨跌对比(%)", + fontsize=14, + fontweight="bold", + ) + plt.xlabel("Symbol", fontsize=12) + plt.ylabel("涨跌幅(%)", fontsize=12) + plt.xticks(x, bar_data["symbol"], rotation=45, ha="right") + plt.legend() + plt.grid(True, alpha=0.3) + + # 在量化策略涨跌柱状图上方添加数值标签 + for i, (bar1, value1) in enumerate( + zip(bars1, bar_data[column_name_text]) + ): + plt.text( + bar1.get_x() + bar1.get_width() / 2, + value1 + (0.01 if value1 >= 0 else -0.01), + f"{value1:.3f}%", + ha="center", + va="bottom" if value1 >= 0 else "top", + fontsize=9, + fontweight="bold", + color="darkblue", ) - plt.title(f"{bar}趋势{column_name_text}(%)") - plt.xlabel("symbol") - plt.ylabel(column_name_text) - plt.xticks(rotation=45, ha="right") - # 在柱状图上添加数值标签 - for i, v in enumerate(bar_data[column_name_text]): - ax.text( - i, - v, - f"{v:.3f}", - ha="center", - va="bottom", - fontsize=10, - fontweight="bold", - ) + # 在市场自然涨跌柱状图上方添加数值标签 + for i, (bar2, value2) in enumerate( + zip(bars2, bar_data["市场自然涨跌"]) + ): + plt.text( + bar2.get_x() + bar2.get_width() / 2, + value2 + (0.01 if value2 >= 0 else -0.01), + f"{value2:.3f}%", + ha="center", + va="bottom" if value2 >= 0 else "top", + fontsize=9, + fontweight="bold", + color="darkgreen", + ) plt.tight_layout() - save_path = os.path.join( - self.stats_chart_dir, - f"{bar}_bar_chart_{column_name}_{strategy_name}.png", - ) + if self.commission_per_share > 0: + save_path = os.path.join( + self.stats_chart_dir, + f"{bar}_bar_chart_{column_name}_{strategy_name}_with_commission.png", + ) + else: + save_path = os.path.join( + self.stats_chart_dir, + f"{bar}_bar_chart_{column_name}_{strategy_name}_without_commission.png", + ) + plt.savefig(save_path, dpi=150) plt.close() sheet_name = f"{bar}_趋势{column_name_text}柱状图_{strategy_name}" chart_dict[sheet_name] = save_path return chart_dict - - def draw_quant_line_chart(self, data: pd.DataFrame, strategy_name: str = "全均线策略"): + + def draw_quant_line_chart( + self, + data: pd.DataFrame, + market_data_pct_chg_df: pd.DataFrame, + strategy_name: str = "全均线策略", + ): """ 根据量化策略买卖明细记录,绘制量化策略涨跌与市场自然涨跌的折线图 :param data: 量化策略买卖明细记录 + :param market_data_pct_chg_df: 市场自然涨跌记录 :param strategy_name: 策略名称 :return: None """ @@ -731,16 +912,27 @@ class MaBreakStatistics: chart_dict = {} for symbol in symbols: for bar in bars: - symbol_bar_data = data[(data["symbol"] == symbol) & (data["bar"] == bar)] + symbol_bar_data = data[ + (data["symbol"] == symbol) & (data["bar"] == bar) + ] if symbol_bar_data.empty: continue - + # 获取第一行数据作为基准 first_row = symbol_bar_data.iloc[0].copy() - + initial_capital = int( + market_data_pct_chg_df.loc[ + (market_data_pct_chg_df["symbol"] == symbol) + & (market_data_pct_chg_df["bar"] == bar), + "initial_capital", + ].values[0] + ) # 创建初始化行,设置基准值 init_row = first_row.copy() - init_row.loc["pct_chg_total"] = 1.0 # 量化策略初始值为1 + init_row.loc["profit_loss"] = 0 + init_row.loc["end_account_value"] = ( + initial_capital # 量化策略初始值为初始资金 + ) init_row.loc["end_timestamp"] = first_row["begin_timestamp"] init_row.loc["end_date_time"] = first_row["begin_date_time"] init_row.loc["end_close"] = first_row["begin_close"] @@ -756,73 +948,121 @@ class MaBreakStatistics: init_row.loc["interval_minutes"] = 0 init_row.loc["interval_hours"] = 0 init_row.loc["interval_days"] = 0 - + # 将初始化行添加到数据开头 symbol_bar_data = pd.concat([pd.DataFrame([init_row]), symbol_bar_data]) - symbol_bar_data.sort_values(by="end_timestamp", ascending=True, inplace=True) + symbol_bar_data.sort_values( + by="end_timestamp", ascending=True, inplace=True + ) symbol_bar_data.reset_index(drop=True, inplace=True) - + # 确保时间列是datetime类型,避免matplotlib警告 - symbol_bar_data["end_date_time"] = pd.to_datetime(symbol_bar_data["end_date_time"]) - + symbol_bar_data["end_date_time"] = pd.to_datetime( + symbol_bar_data["end_date_time"] + ) + + # 计算持仓价值归一化数据(相对于初始价格) + symbol_bar_data["end_account_value_to_1"] = ( + symbol_bar_data["end_account_value"] / initial_capital + ) + symbol_bar_data["end_account_value_to_1"] = symbol_bar_data[ + "end_account_value_to_1" + ].round(4) + # 计算市场价位归一化数据(相对于初始价格) - symbol_bar_data["end_close_to_1"] = symbol_bar_data["end_close"] / init_row["end_close"] - symbol_bar_data["end_close_to_1"] = symbol_bar_data["end_close_to_1"].round(4) - + symbol_bar_data["end_close_to_1"] = ( + symbol_bar_data["end_close"] / init_row["end_close"] + ) + symbol_bar_data["end_close_to_1"] = symbol_bar_data[ + "end_close_to_1" + ].round(4) + # 绘制折线图 plt.figure(figsize=(12, 7)) - + # 绘制量化策略涨跌线(蓝色) - plt.plot(symbol_bar_data["end_date_time"], symbol_bar_data["pct_chg_total"], - label="量化策略涨跌", color='blue', linewidth=2, marker='o', markersize=4) - + plt.plot( + symbol_bar_data["end_date_time"], + symbol_bar_data["end_account_value_to_1"], + label="量化策略涨跌", + color="blue", + linewidth=2, + marker="o", + markersize=4, + ) + # 绘制市场自然涨跌线(绿色) - plt.plot(symbol_bar_data["end_date_time"], symbol_bar_data["end_close_to_1"], - label="市场自然涨跌", color='green', linewidth=2, marker='s', markersize=4) - - plt.title(f"{symbol} {bar} 量化与市场折线图_{strategy_name}", - fontsize=14, fontweight='bold') + plt.plot( + symbol_bar_data["end_date_time"], + symbol_bar_data["end_close_to_1"], + label="市场自然涨跌", + color="green", + linewidth=2, + marker="s", + markersize=4, + ) + + plt.title( + f"{symbol} {bar} 量化与市场折线图_{strategy_name}", + fontsize=14, + fontweight="bold", + ) plt.xlabel("时间", fontsize=12) plt.ylabel("涨跌变化", fontsize=12) plt.legend(fontsize=11) plt.grid(True, alpha=0.3) - + # 设置x轴标签,避免matplotlib警告 # 选择合适的时间间隔显示标签,避免过于密集 if len(symbol_bar_data) > 30: # 如果数据点较多,选择间隔显示,但确保第一条和最后一条始终显示 step = max(1, len(symbol_bar_data) // 30) - + # 创建标签索引列表,确保包含首尾数据 label_indices = [0] # 第一条 - + # 添加中间间隔的标签 for i in range(step, len(symbol_bar_data) - 1, step): label_indices.append(i) - + # 添加最后一条(如果还没有包含的话) if len(symbol_bar_data) - 1 not in label_indices: label_indices.append(len(symbol_bar_data) - 1) - + # 设置x轴标签 - plt.xticks(symbol_bar_data["end_date_time"].iloc[label_indices], - symbol_bar_data["end_date_time"].iloc[label_indices].dt.strftime('%Y%m%d %H:%M'), - rotation=45, ha='right') + plt.xticks( + symbol_bar_data["end_date_time"].iloc[label_indices], + symbol_bar_data["end_date_time"] + .iloc[label_indices] + .dt.strftime("%Y%m%d %H:%M"), + rotation=45, + ha="right", + ) else: # 如果数据点较少,全部显示 - plt.xticks(symbol_bar_data["end_date_time"], - symbol_bar_data["end_date_time"].dt.strftime('%Y%m%d %H:%M'), - rotation=45, ha='right') - + plt.xticks( + symbol_bar_data["end_date_time"], + symbol_bar_data["end_date_time"].dt.strftime("%Y%m%d %H:%M"), + rotation=45, + ha="right", + ) + plt.tight_layout() - - save_path = os.path.join( - self.stats_chart_dir, - f"{symbol}_{bar}_line_chart_{strategy_name}.png", - ) - plt.savefig(save_path, dpi=150, bbox_inches='tight') + + if self.commission_per_share > 0: + save_path = os.path.join( + self.stats_chart_dir, + f"{symbol}_{bar}_line_chart_{strategy_name}_with_commission.png", + ) + else: + save_path = os.path.join( + self.stats_chart_dir, + f"{symbol}_{bar}_line_chart_{strategy_name}_without_commission.png", + ) + + plt.savefig(save_path, dpi=150, bbox_inches="tight") plt.close() - + sheet_name = f"{symbol}_{bar}_折线图_{strategy_name}" chart_dict[sheet_name] = save_path return chart_dict diff --git a/market_monitor_main.py b/market_monitor_main.py index dd7af5e..a67d615 100644 --- a/market_monitor_main.py +++ b/market_monitor_main.py @@ -4,7 +4,7 @@ from huge_volume_main import HugeVolumeMain from core.biz.market_monitor import create_metrics_report from core.db.db_market_monitor import DBMarketMonitor from core.wechat import Wechat -from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WECHAT_CONFIG +from config import OKX_MONITOR_CONFIG, OKX_REALTIME_MONITOR_CONFIG, MYSQL_CONFIG, WECHAT_CONFIG from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp import core.logger as logging @@ -52,6 +52,14 @@ class MarketMonitorMain: else: self.is_binance = True + + self.symbols = OKX_REALTIME_MONITOR_CONFIG.get("volume_monitor", {}).get( + "symbols", ["XCH-USDT"] + ) + self.bars = OKX_REALTIME_MONITOR_CONFIG.get("volume_monitor", {}).get( + "bars", ["1m", "5m", "15m", "30m", "1H"] + ) + def get_latest_record(self): """ 获取最新记录 @@ -252,13 +260,13 @@ class MarketMonitorMain: 获取下一个长周期实时数据 """ if next: - # 获得bar在self.market_data_main.bars中的索引 - bar_index = self.market_data_main.bars.index(bar) - if bar_index == len(self.market_data_main.bars) - 1: + # 获得bar在self.bars中的索引 + bar_index = self.bars.index(bar) + if bar_index == len(self.bars) - 1: logger.error(f"已经是最后一个bar: {bar}") return None # 获得下一个bar - bar = self.market_data_main.bars[bar_index + 1] + bar = self.bars[bar_index + 1] # 获得下一个bar的实时数据 data = self.market_data_main.market_data.get_realtime_kline_data( symbol=symbol, bar=bar, end_time=end_time, limit=100 @@ -278,11 +286,11 @@ class MarketMonitorMain: only_output_over_mean_volume: bool = True, only_output_rise: bool = True, ): - for symbol in self.market_data_main.symbols: + for symbol in self.symbols: if self.is_binance and symbol == "XCH-USDT": logger.info(f"币安交易所无: {symbol}") continue - for bar in self.market_data_main.bars: + for bar in self.bars: logger.info( f"开始监控: {symbol} {bar} 窗口大小: {self.window_size} 行情数据" ) @@ -305,7 +313,7 @@ if __name__ == "__main__": market_monitor_main = MarketMonitorMain() market_monitor_main.monitor_realtime_market( symbol="BTC-USDT", - bar="5m", + bar="1m", only_output_huge_volume=False, only_output_rise=False, ) diff --git a/trade_ma_strategy_main.py b/trade_ma_strategy_main.py index 338c2fa..b7df31c 100644 --- a/trade_ma_strategy_main.py +++ b/trade_ma_strategy_main.py @@ -24,42 +24,48 @@ from config import ( logger = logging.logger + class TradeMaStrategyMain: - def __init__(self, is_us_stock: bool = False, is_binance: bool = False): - self.ma_break_statistics = MaBreakStatistics(is_us_stock=is_us_stock, is_binance=is_binance) - + def __init__( + self, + is_us_stock: bool = False, + is_binance: bool = False, + commission_per_share: float = 0, + ): + self.ma_break_statistics = MaBreakStatistics( + is_us_stock=is_us_stock, + is_binance=is_binance, + commission_per_share=commission_per_share, + ) + def batch_ma_break_statistics(self): """ 批量计算MA突破统计 """ logger.info("开始批量计算MA突破统计") strategy_dict = self.ma_break_statistics.main_strategy - pct_chg_df_list = [] + account_value_chg_df_list = [] for strategy_name, strategy_info in strategy_dict.items(): if "macd" in strategy_name: # 只计算macd策略 - pct_chg_df = self.ma_break_statistics.batch_statistics(strategy_name=strategy_name) - pct_chg_df_list.append(pct_chg_df) - - pct_chg_df = pd.concat(pct_chg_df_list) - - def statistics_pct_chg(self, pct_chg_df: pd.DataFrame): - """ - 1. 将各个symbol, 各个bar, 各个策略的pct_chg_total构建为新的数据结构,如: - symbol, bar, stratege_name_1, stratege_name_2, stratege_name_3, ... - stratege_name_1的值, 为该策略的pct_chg_total的值 - 2. 构建新的数据结构: symbol, bar, max_pct_chg_total_strategy_name, min_pct_chg_total_strategy_name - 如: BCT-USDT, 15m, 均线macd结合策略2, 全均线策略 - 3. 构建新的数据结构, bar, max_pct_chg_total_strategy_name, min_pct_chg_total_strategy_name - 如: 15m, 均线macd结合策略2, 全均线策略 - 4. 构建新的数据结构, symbol, max_pct_chg_total_strategy_name, min_pct_chg_total_strategy_name - 如: BCT-USDT, 均线macd结合策略2, 全均线策略 - """ - logger.info("开始统计pct_chg") - - + account_value_chg_df = self.ma_break_statistics.batch_statistics( + strategy_name=strategy_name + ) + account_value_chg_df_list.append(account_value_chg_df) + + total_account_value_chg_df = pd.concat(account_value_chg_df_list) + return total_account_value_chg_df + + def statistics_account_value_chg(self, account_value_chg_df: pd.DataFrame): + logger.info("开始统计account_value_chg") if __name__ == "__main__": - trade_ma_strategy_main = TradeMaStrategyMain(is_us_stock=False, is_binance=True) - trade_ma_strategy_main.batch_ma_break_statistics() \ No newline at end of file + commission_per_share_list = [0, 0.0008] + for commission_per_share in commission_per_share_list: + trade_ma_strategy_main = TradeMaStrategyMain( + is_us_stock=False, + is_binance=True, + commission_per_share=commission_per_share, + ) + trade_ma_strategy_main.batch_ma_break_statistics()