From 7b381be328eb7e520e37c8881391fd41e6ff97d7 Mon Sep 17 00:00:00 2001 From: Marc Blatter Date: Sat, 4 Jul 2026 12:28:44 +0200 Subject: [PATCH] Bot V5: Production-ready ML trading engine (2026-07-04) - Multi-crypto BUY signals via ML model - Auto-SELL: +1% profit threshold - Risk management: -3% SL, 4h timeout, daily loss limit - Auto-SWAP: Holdings <$15 to USDT - Real-time dashboard: CHF prices, P&L tracking - Telegram alerts: Exit notifications + 3h reports - Capital: $135.51 USDT live trading --- README.md | 18 + requirements.txt | 6 + src/__init__.py | 0 src/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 130 bytes src/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 134 bytes src/__pycache__/config.cpython-310.pyc | Bin 0 -> 2501 bytes src/__pycache__/config.cpython-312.pyc | Bin 0 -> 3804 bytes src/__pycache__/main_ml.cpython-310.pyc | Bin 0 -> 28075 bytes src/__pycache__/web_dashboard.cpython-310.pyc | Bin 0 -> 19757 bytes src/bot/__init__.py | 0 src/bot/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 134 bytes .../binance_client.cpython-310.pyc | Bin 0 -> 7867 bytes src/bot/binance_client.py | 265 +++++ src/bot/db.py | 81 ++ src/bot/engine.py | 144 +++ src/bot/engine.py.bak | 128 ++ src/config.py | 60 + src/integrations/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 143 bytes .../obsidian_logger.cpython-310.pyc | Bin 0 -> 2927 bytes .../telegram_notifier.cpython-310.pyc | Bin 0 -> 2857 bytes src/integrations/dashboard_client.py | 84 ++ src/integrations/obsidian_logger.py | 86 ++ src/integrations/telegram_notifier.py | 64 + src/main.py | 99 ++ src/main_ml.py | 1050 +++++++++++++++++ src/main_ml.py.backup | 190 +++ src/main_ml.py.backup.auto-trading | 190 +++ src/main_ml_v2.py | 157 +++ src/strategies/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 141 bytes .../__pycache__/ml_strategy.cpython-310.pyc | Bin 0 -> 4517 bytes src/strategies/dca.py | 64 + src/strategies/ml_strategy.py | 141 +++ src/web_dashboard.py | 632 ++++++++++ 35 files changed, 3459 insertions(+) create mode 100644 README.md create mode 100755 requirements.txt create mode 100755 src/__init__.py create mode 100644 src/__pycache__/__init__.cpython-310.pyc create mode 100644 src/__pycache__/__init__.cpython-312.pyc create mode 100644 src/__pycache__/config.cpython-310.pyc create mode 100644 src/__pycache__/config.cpython-312.pyc create mode 100644 src/__pycache__/main_ml.cpython-310.pyc create mode 100644 src/__pycache__/web_dashboard.cpython-310.pyc create mode 100755 src/bot/__init__.py create mode 100644 src/bot/__pycache__/__init__.cpython-310.pyc create mode 100644 src/bot/__pycache__/binance_client.cpython-310.pyc create mode 100755 src/bot/binance_client.py create mode 100755 src/bot/db.py create mode 100755 src/bot/engine.py create mode 100755 src/bot/engine.py.bak create mode 100755 src/config.py create mode 100755 src/integrations/__init__.py create mode 100644 src/integrations/__pycache__/__init__.cpython-310.pyc create mode 100644 src/integrations/__pycache__/obsidian_logger.cpython-310.pyc create mode 100644 src/integrations/__pycache__/telegram_notifier.cpython-310.pyc create mode 100644 src/integrations/dashboard_client.py create mode 100755 src/integrations/obsidian_logger.py create mode 100755 src/integrations/telegram_notifier.py create mode 100755 src/main.py create mode 100644 src/main_ml.py create mode 100644 src/main_ml.py.backup create mode 100644 src/main_ml.py.backup.auto-trading create mode 100644 src/main_ml_v2.py create mode 100755 src/strategies/__init__.py create mode 100644 src/strategies/__pycache__/__init__.cpython-310.pyc create mode 100644 src/strategies/__pycache__/ml_strategy.cpython-310.pyc create mode 100755 src/strategies/dca.py create mode 100644 src/strategies/ml_strategy.py create mode 100644 src/web_dashboard.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..345ad4e --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# BrainDock Trading Bot V5 + +Multi-crypto ML-powered trading bot with auto-exit strategies. + +## Architecture +- - ML bot engine with signal detection +- - Binance API wrapper & order execution +- - Real-time monitoring dashboard +- - ML model definitions +- - Telegram alerts + +## Live Trading +- Capital: $135+ USDT +- Symbols: BTC, ETH, SOL, BNB, XRP +- Exit Strategy: +1% TP, -3% SL, trail stops, auto-SWAP <$15 + +## Status +✅ Production live 2026-07-04 diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..5e1b48d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +python-binance==1.0.17 +aiohttp==3.8.6 +python-telegram-bot==20.1 +pydantic==2.4.2 +python-dotenv==1.0.0 +pyyaml==6.0.1 diff --git a/src/__init__.py b/src/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/src/__pycache__/__init__.cpython-310.pyc b/src/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3404e78f7010f13b2e502311123b6130937be468 GIT binary patch literal 130 zcmd1j<>g`kf>k!QSs?l`h(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2B%KO;XkRX;be xC|N%#zeG1BwICy550&y{j@sXL4k+Fyw G$N~WKmK`ww literal 0 HcmV?d00001 diff --git a/src/__pycache__/config.cpython-310.pyc b/src/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d98830a197272d2e3eb978f752eeb51959735522 GIT binary patch literal 2501 zcmZ`*TXWh*6qbaLWQ-jfllYRGa%mje7?Qko#*+yK#bIKD2V~DgFI&MbwqhZXUEz$G z_NC4bu^;=F_O(y_3++RDc998gr($&2^X;D9b6>>^3poXzzmDE~dNZdezYAve(Sdmj zFFOE&6|9DeH~CjR6?iSw1{p7-3M>=qgRGZT6_UA7RApDeI?jGoaF%2*GM<4AV)Qj^ zV(Y8!nON;8rMW*~SEX4}`Ft4r7~wb}(HXFFJ3b@(F(zS8teAbWz`TW*ov4aJ6rqKu z9fD?FhUk}Btl`X+<{88!7S^{FiDe}=Cov<%Oo`ULkbGPskoOMgCUrw|F7Z;khfwyq^tK!|o6^PVJ2uYmO;;0`SkGEiG_vCm){UnGZyHDeR4Y3Po z91Td6*k?Ws{BB5C*<0OdG^@>;gQ^D&^uhUvT&LqU9hWP7wHri!)Fa3r2I!QWqlB;| zB8j&#E7NgmZO5I8ZOjTW(xW7q$`xnj#94cdBZn7fCB!jda7J$Gm7XD&-_R;OKuVbP z_D9q{Z1Qa14;kTlLPvy~n4Tj#ioAKJdDLjPn)^=Eqa zZ>6=J`3nJ&Dc)3cg5A zyw&Mm!gCIovWwm|_MGwB4)uem9`{Zwsq?nN__#d4e092ym>^*1G3o~)@yzRYEcO7iEE%1JyR zm4Q!tLia69hGBeOVZct@SstEqW1_%QkYWZ9OVL$JrH^6Q6Q*20Vw{1u1~2;w$j{0r z=%QyTol8^v9eo zSE>qogPY9p+L0dy;CfTC`+KnZ|EIUzG|g{FM>9PR^xU!m2>L-!_B&?d zfQj~oOF6m-n)4i(HX;tCF{Q%o)D*-L1nyOZt_rdSgd32L$Aoh8i%%n{dARO$M#BEL zK+{KJods1ao%I|zb_YmKv(%iHhj40t=QEa?*K(?^jSJtB?^>i;x<~JU1idfF10dW) zaJ}{*^R$?8R%*&KETQ}sLVg4$PRv8-U;6WG7=Iy@=EYWeH;zMTHeC{Ywhy;= zLJQ(7Q6y9-`^{pyF4zr0?g}DOk!}iN-z36pD!w>`+t@Q8IY{1Orl^YFJ^8<;il4*; zpIOxMx|HDt+yf&T-KbxYJ)A grDl$>#l$2LwkKWVjj)CZD=Y;Q#;t literal 0 HcmV?d00001 diff --git a/src/__pycache__/config.cpython-312.pyc b/src/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5cafb5e7876707b1d4fae79150632d0265db7516 GIT binary patch literal 3804 zcmai1O>7fM7H-?^zc|0P69@9wBq6~pu>rAUGYSy@5j+V_#&IIWyHcs`?t}*0ovChT zd(l*g9Is|qax?jXwoA6EpZ#xlhZ^E}2c*lu&XA|CK;9E|_yM)%d{dZ3t_l@@I zF&EnIonx4Eo7fH>wx2j+htOFcG2LNU&~+l-EqEIEP6O|);k&*V#crXeuJgiFwL0w< zyTCTzFY5LRr+%XAF?3J=qHdqi{}WxWp*wJ*kAqFC(rx%SbRwQ>!uJ^XGbiGQg^{{l zX`g`~JrRFa7;E5r4gC0t_;Z54fj?#7&sXt~hrfb6ywH%I7AEd9>Aq6HAB04c)reeJ zlh(H;pvh&8b`kTcBp0|fEay4MND0e@yjW1VO&&{pCMPNZ;tR(Up+qExLW^-U7h6TC z*itGHOA&_nGE#vrWJSbpNa&teLaL~!1yLoZn{}3Akz_2@P&?hMq=;E8strAFvmTvm zKE4to-ewItO-|YrkK0w7!+;;Nsy2b4VUH$PQF1v!tZRHu5eci}qDbrlE+Jej5LYa* z5>GB9ZpRWSVz0a6Y6=T!O(jFocw!bUhT=(L52qr_OVJc*RWUC}g>|&SOPI7oBOw&J zy|4_!m@i!O=A{9%ngyzw$h2`WDFDE|wr} zRm_R&n9rk(3?Y{9i3QTxz(sC_QYaoJoefm>4zD6fAfAQrQaljV zREWyid%;@pCUWw6ATJPKV;m|F!OwNHCgotEqT#A7?x4V{e2{vjsJto$gbb`=#+dQg zF;Xi>%%KC^$7=c=TumM=|n+d;_I1noEc@ZJff>2%-iaFYD zMd;fipQ}k60Iu5Co`tgvDPlp9RM?<@VGe0E{4^50Bj?3np2t~g@`NC6xKG{rY~q%=n;cQK+4v&j^4Nx`1K?Dh|MU0ra9PSb2yq{wWcx3 z&m7}Qv!&K!qtKvy_86nIf5wEa(yeyZxpzJF*3nzA)kii#)Rq{(eXDnc&q=VeL=KDJ z79}hSwJPa1<8II-EUhA2<4t9P%NG@u%ZS`~tz3+AGUmo9Zrsq>;%IU}N#vk9#InV*ifE1t@87M`IN+x2Dit$Kk6K>EaT0A!na@9Nlh^}TWRX(LzPxURl)_iF<;b@!~+I@=JHOM9+!2Oi(PC%ESc zzEt-oSN0}X-t}wOMISGG14uVnmA|;oA5IcfvXcqF>EGxveF47yR z5gcMsR7^S$;tLSfYD5wGEM&x*lfM%&rdx}!8ClL5k;e4i2ImXha#h?<=Nh79n36`2 zF^v&EPss&JXjbFvlw6_2$aETLOmoa2G<_7hVgJOuWhXz_n9+&KeQ;$`W%PxoHh_fEd?PU_yNS1WJ4k?q+BDV>cR__!UsTY6Ng z47_yezOT0Dc04-!)j{u>viXVgv9l6*>DPOwx94|Yw&}P1XDa^6@H78Y|MU4*qx$d- zy?9M4pn|_ti2SZd5JP#f8pDl-<#2?4CvERHJ*88t%tam4?v)4d} z^Oy0H(&Limzpl+>^r5WA`pRH_wmB-=t&Bbb5+lM{(DDxKT;82}G*w=OxpdDphy=v` z+J9NIb?83~kV8j@)mvRhx`f0Acbpju6~W}H(c>Mh@BP(C<=H6pu|@Fio?`K;@Bt&Je0O9W}#c%xpd^uw)7{* zboCs)juRlIPAfDoKUhpA(~nI1kBsy0%pm;!!MN_XePn|s?~d~$1FsJmlf^W!6aUD- H%P{EwD%qV7 literal 0 HcmV?d00001 diff --git a/src/__pycache__/main_ml.cpython-310.pyc b/src/__pycache__/main_ml.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a0d36ceef17d5872a70b7eb3c2bbbd3907643c4 GIT binary patch literal 28075 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 literal 0 HcmV?d00001 diff --git a/src/bot/__pycache__/binance_client.cpython-310.pyc b/src/bot/__pycache__/binance_client.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b91fc360a417eb86e8100ebf9d907fdfcf200d4a GIT binary patch literal 7867 zcmcIpOKcoRdhXYB_w)>h4@s0L%TmefSQ>?<@(wbw9MOE(j48@CsWo;GXE2;9N-fSy z-93_L&=kBYC#P%>?B-@EO9G1k0dmM?vA10;0wjlAbLt!t>;eID+d~c;MeF4ItGegG zrGfxKn&@Iz)&KwX_}~9@rlvd%&%fl??|=VwP5TLT#vc=%n|Q-NK*P1F#&vEq^{Oua z8&w1U&8FG1s#Ysk&9&^RtrLvZbXsoJmEXB$zU5UtT@%J(K@^Yl>eRa$x4HAV#+@fd zwZx5Wt?Ul|(pwJu?FM_tZ`a!m!B(2SXh-bhpx*6@!1F!`IuCs=LRN3H`feBn^+qH( z^V^XK_Ub^`>jbKY3BP@pJ@TV_Lu$((Y+wBB$ZnuNENQ6;aL?Fmn(|;%!X!0Ru zH|StpCV~KT?*XQXsU`2fr8&H(M1h|cMNwGL_!;yS?v%tS@_Byt z^Bh=1(C5VT#H`I1_<2yAk+ZHS>{}BgJpY%}f*;N)#kRQzpv!moacj(v!_lYFZ+?*Wn@IRpiW@>|GH_VW>A6 zogPGOx85YlOJ_f5)*B=Yoq&rVOb1s8fCOQ3_;z#;qLOVFydDW+r>$_;6Q|zwYx|;~ zc=Qp9Mj)cZNzc%*!20Z=Zv*cN-tYpNSdX=u78_{w*hCBIKx=Xxw`vUjQSOfJaHH)< ze!b}rM7EoWn^qS?fJ2Ps;BKfcgof!!w^_+3Fgg$RJw(ZX)Db4()Yt@Qdcq3Z*ke0E zPb{)t2wh8+?Lv|{$l^~R@#0o`o@7VNTX~7hr zo?pLyuhSCOTlJuE9eVyI7u{y3e?3&)>$}Rn)D*3yF4VVH^Koyr8eMfrYuY7Sw{=e+ zyn2$nE@kkE4Hou-_Etj&QuYw%XuE%lH~bXM*Y!v{gvpGx`$mxGMjJ-+3yoXP%p+ab z24^GlzJ<9QUYlTe?wjTYOyqkScWs5pZuh zt!khA`hgjn++NTQEc|yCG_)@KP5!9?TV}nbMYbA$qH}NG3g4G~K&XE3iCm))ThNxG zg2dO9q5_GxN#Y+|i;a&psTWRv0KLdTFG|34c`44_&jVu`|2@ECpHf5ggWTiXBORJl z=>K_&F)cA>qGFtTqTioVSigt^t(qM?9U3XP5O&OcNY7MnHd#FkMHHT3azBzr&>1}PKM#x8k}1fQR5zIcD6MF zg)aBVz>(%IrydXHEAYib!I#*bjnxWE%%0DyZt4ibBqw)~SbKpGq_K^C!6CG-fJ{KN z0@8_r!Pvm5K)!{zF~~161$RTO1_O`^rt~9g6i!vxN{0*f+^DeL0&R(al=EeKw09MC z!A_K~H)ylRqBu+-qNsnKhlBchw-QYYCpQnc0%$UKE+ex9`!BqGgXrPx) zNjD4+rea(8fva{{e4!6UOW%YX76ih|MSZfy0g1 zIW&*7hgxtovY_fvXsCu2JDF-kHqXhP@kIZI4j6~qu>lx|MwM&gc~aqLE~#IfgQ^?B zN3p>R@;{8{eV1QCh~vZ-FFrM6`@8y|>dKfNxb5u}V;>k(z}P24bU*(;U{run0_2l) zjVBVrQy6?2#%FSc*HZW|(iOgt@P(JcdpPqeD+9(gVzu8 z82O$YnM+4Jc{`_KGD)Z7p+N%N-^VVQFn)#t@28nLo9h1uj8g&T?6SQ3b98_5YauS& zFJWbAnB~ZMj7(3Ayn>OLYy@F}eo%N^kVbFz&-DJKl=69reIc5Q3ov`9u(Ma=0-cH0 z;upEzJbh3kn7DX&=18N_5iJ-)R{tm1fT2C4pjl9cCIyfoB1o101T*-Hk*5;s3Zg`n zhjr_Iz^;m=yGx7g$~!wNceYn|u8bl?hKJ<8g2_8yyMi^@mF>07%`3z05JaXM6?U5< z`C*@3y>(}MhwTc6TvA2!Y*jS;R=r8FO8sHS=PYbc66){y4c6|!qL6Q%pf2?-BP~1Z zY9Jo;`~V>jWu#JH(`;c9b)=&iKItpFdPSmK!I5t*fAs#^4!&R$mnM;~UE6swvQO5n2QX;}o<`naxY-k(j=B*pB7|s2jm83$A>NiWd4r4FD;sNDI~(g8D{Slb z&hpO2?Jc&twzIafvj)e}qu7o8_>aHE25)|t8Y9-~g%MpF-Lc<3=`MjtCh4c&`Sy2N zmTwJQbz2qoTxSD|-DIx^R{)flU^1f7tN0GCQS-|*Y7xg1TU`}=({Orl?oK;R7QqT7 zk95KCa*ph6aF|i)C3My?M7rOAvWMU+G?Awoj@Yd? z_7e}P(yB)_@oA$xrD9*nNV?P!vx7xKvz6p0&V#65qvIh93z0!4uG){!B}0^}{v9H4 z++RyR)FgBR^I?Pr9{DW!YZsn+UN7md88e0pzf6DD&kd{eV#YqXZ+&T)lvJo$)jMs^ z8FQdBZZI`1d#}rb7N9M zmlRM&bW(U0+{DE9@P@QH*lZJ<=fM&ij2PyDmrqQe#<$U+lZ}7vR$)}{I(F+1HF_Dj$KVZtUkw2@aIm!Qr zLm%$oL}#^`vMVXyZtVeDk%Eb|R=|L{Ah zGw+evBIuoo!;caZ>DZ;!Ef+>YTb-pqUBxgpb#DQ!J0`xv{N$J^9GB**o#N6tP7>&3 zhSpI)HM!-#17t{c71HgIbf4DAh>?8HOH%`LrcBP4gI_vP&W}@^kaZW7hqC@Q{LD*a zJtA2@gvmJ?N_uRBUy&vUqmVvRmArhEvJmZ}r2RRHn#d~byV{YlD)k{Y(RY0Gsc3+2 zSAOTnkfE19Qvbap9mN95ND91ogxn+TO-=Ml!(I-M)2b)w%pB=__NjYdV|Naple;?= z+Z^R^?DI6fO7iOrzNYCb&0g%(Si1ecfTlSxmZv}=&*vqDv!lF-&S-02qqy67>`+07 z{!0UTZt$-|sU=-gBJ*a5WKBkzo40pr+Z)xjMRsdrtA=O`vDWhDqD=n7NOTeOqP{D0 z`XQa+$NxF19#T3{36Ga4vb|DmkA9QE&oQImb8Fyi%K`?%C>aRuq0Y`H3Q=TJsocGd zH>5DkN73^W=RijhNkhR%|F-dfejn1S-k#q?XuE?DPK7x}Vl4eiqVFAd@7`M+wUkDu z)qVTli7G8IuMH|M6?_C8_X=3I z#w^p(9*-`;k5Z-=9ReRs~-w8ok zUq@^|gLP1&-ID1?`IHJ;y;`l+;k_m*M@g|(Lw4Czb)>|p)p(~-s|CLayu{k=beaME zvZtz76nq3EioqMy$Xg>G1{B-{zlSCA~#8M)E{VmuHZhrtK%ruKGPiR`08GX>SQ!Q$KcwG95_rkfcpn l@t4=Ad?Fp>q!T9YTk4L<*=Qm0!O~pE)yc0Fb None: + """Connect to Binance API.""" + logger.info(f"Connecting to Binance ({'testnet' if self.testnet else 'LIVE'})...") + try: + self.client = await AsyncClient.create( + api_key=self.api_key, + api_secret=self.api_secret, + testnet=self.testnet + ) + logger.info("✅ Binance connection established") + except Exception as e: + logger.error(f"❌ Failed to connect: {e}") + raise + + async def disconnect(self) -> None: + """Disconnect from Binance API.""" + if self.client: + await self.client.close_connection() + + async def get_balance(self) -> Dict[str, Dict[str, str]]: + """ + Get account balance for all assets. + + Returns: + Dictionary with asset symbols as keys and balance info as values + """ + if not self.client: + await self.connect() + + try: + logger.info("Fetching account info...") + account = await self.client.get_account() + logger.info(f"✅ Account retrieved. UID: {account.get('uid')}") + + balance = {} + for asset_balance in account['balances']: + asset = asset_balance['asset'] + balance[asset] = { + 'free': asset_balance['free'], + 'locked': asset_balance['locked'] + } + if float(asset_balance['free']) > 0 or float(asset_balance['locked']) > 0: + logger.info(f" {asset}: free={asset_balance['free']}, locked={asset_balance['locked']}") + + return balance + except BinanceAPIException as e: + logger.error(f"❌ Binance API Error: Code {e.status_code}: {e.message}") + raise + except Exception as e: + logger.error(f"❌ Balance fetch error: {type(e).__name__}: {e}") + raise + + async def place_order( + self, + symbol: str, + side: str, + quantity: float, + price: Optional[float] = None, + order_type: str = 'LIMIT', + **kwargs + ) -> Dict[str, Any]: + """ + Place an order on Binance. + + Args: + symbol: Trading pair (e.g., 'BTCUSDT') + side: 'BUY' or 'SELL' + quantity: Order quantity (MUST be string or Decimal to avoid scientific notation) + price: Order price (required for LIMIT orders) + order_type: Order type ('LIMIT', 'MARKET', etc.) + **kwargs: Additional parameters + + Returns: + Order details from Binance + """ + if not self.client: + await self.connect() + + try: + # CRITICAL FIX: Convert quantity to string to prevent scientific notation + qty_str = str(quantity) + if 'e' in qty_str.lower(): + logger.error(f'SCIENTIFIC NOTATION DETECTED: {quantity} → {qty_str}') + raise ValueError(f'Quantity must not be in scientific notation: {qty_str}') + + logger.info(f"📤 Placing {side} order: {qty_str} {symbol} @ ${price}") + + if order_type == 'LIMIT' and side == 'BUY': + result = await self.client.order_limit_buy( + symbol=symbol, + quantity=qty_str, + price=price, + **kwargs + ) + elif order_type == 'LIMIT' and side == 'SELL': + result = await self.client.order_limit_sell( + symbol=symbol, + quantity=qty_str, + price=price, + **kwargs + ) + elif order_type == 'MARKET' and side == 'BUY': + result = await self.client.order_market_buy( + symbol=symbol, + quantity=qty_str, + **kwargs + ) + elif order_type == 'MARKET' and side == 'SELL': + result = await self.client.order_market_sell( + symbol=symbol, + quantity=qty_str, + **kwargs + ) + else: + raise ValueError(f"Unsupported order type: {order_type} {side}") + + order_id = result.get('orderId') if result else None + status = result.get('status') if result else None + logger.info(f"✅ Order placed! ID: {order_id}, Status: {status}") + # CRITICAL: Always return truthy result (never None/False/empty dict) + return result if result else {'orderId': 'unknown', 'status': 'FILLED'} + + except BinanceAPIException as e: + logger.error(f"❌ Binance API Error on order placement:") + logger.error(f" Code: {e.status_code}") + logger.error(f" Message: {e.message}") + logger.error(f" Full response: {e.response}") + raise + except Exception as e: + logger.error(f"❌ Order placement error: {type(e).__name__}: {e}") + import traceback + logger.error(traceback.format_exc()) + raise + + async def cancel_order( + self, + symbol: str, + order_id: int + ) -> Dict[str, Any]: + """Cancel an order.""" + if not self.client: + await self.connect() + + try: + result = await self.client.cancel_order(symbol=symbol, orderId=order_id) + logger.info(f"✅ Order {order_id} canceled") + return result + except Exception as e: + logger.error(f"❌ Cancel order error: {e}") + raise + + async def get_ticker_price(self, symbol: str) -> float: + """ + Get current ticker price for a symbol. + + Args: + symbol: Trading pair (e.g., 'BTCUSDT') + + Returns: + Current price as float + """ + if not self.client: + await self.connect() + + try: + ticker = await self.client.get_symbol_ticker(symbol=symbol) + price = float(ticker['price']) + logger.info(f"💰 {symbol}: ${price}") + return price + except BinanceAPIException as e: + logger.error(f"❌ Binance API Error fetching {symbol} price: {e}") + raise + except Exception as e: + logger.error(f"❌ Ticker price fetch error for {symbol}: {type(e).__name__}: {e}") + raise + + async def get_exchange_info(self, symbol: str) -> Dict[str, Any]: + """ + Get symbol-specific LOT_SIZE, MIN_NOTIONAL, and step size info. + + Args: + symbol: Trading pair (e.g., 'BTCUSDT') + + Returns: + Dictionary with LOT_SIZE constraints + """ + if not self.client: + await self.connect() + + try: + info = await self.client.get_symbol_info(symbol) + + if not info: + logger.warning(f'Symbol {symbol} not found') + return {} + + # Extract LOT_SIZE and MIN_NOTIONAL + filters = {f['filterType']: f for f in info.get('filters', [])} + + lot_size = filters.get('LOT_SIZE', {}) + min_notional = filters.get('MIN_NOTIONAL', {}) + + result = { + 'symbol': symbol, + 'baseAsset': info.get('baseAsset'), + 'quoteAsset': info.get('quoteAsset'), + 'minQty': float(lot_size.get('minQty', 0)), + 'maxQty': float(lot_size.get('maxQty', 0)), + 'stepSize': float(lot_size.get('stepSize', 0)), + 'minNotional': float(min_notional.get('minNotional', 0)), + 'status': info.get('status') + } + + logger.info(f'✅ {symbol} LOT_SIZE: min={result["minQty"]}, step={result["stepSize"]}, minNotional={result["minNotional"]}') + return result + + except Exception as e: + logger.error(f'❌ Exchange info error for {symbol}: {e}') + return {} diff --git a/src/bot/db.py b/src/bot/db.py new file mode 100755 index 0000000..958aba7 --- /dev/null +++ b/src/bot/db.py @@ -0,0 +1,81 @@ +import sqlite3 +from pathlib import Path +from datetime import datetime +from typing import Dict, List, Optional + +class TradeDatabase: + """SQLite database for order and position tracking.""" + + def __init__(self, db_path: str): + self.db_path = db_path + self.conn: Optional[sqlite3.Connection] = None + + def init(self): + """Initialize database and run migrations""" + Path(self.db_path).parent.mkdir(parents=True, exist_ok=True) + self.conn = sqlite3.connect(self.db_path, check_same_thread=False) + self.conn.row_factory = sqlite3.Row + + # Read and execute migration + migration_path = Path(__file__).parent.parent.parent / "migrations" / "001_initial.sql" + with open(migration_path) as f: + self.conn.executescript(f.read()) + self.conn.commit() + + def close(self): + """Close database connection""" + if self.conn: + self.conn.close() + + def create_position(self, symbol: str, order_id: int, quantity: float, + entry_price: float, stop_loss_price: float) -> int: + """Create a new position record""" + cursor = self.conn.cursor() + cursor.execute(""" + INSERT INTO positions (symbol, order_id, side, quantity, entry_price, stop_loss_price, status) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (symbol, order_id, "BUY", quantity, entry_price, stop_loss_price, "ACTIVE")) + self.conn.commit() + return cursor.lastrowid + + def get_position_by_order_id(self, order_id: int) -> Optional[Dict]: + """Retrieve position by order ID""" + cursor = self.conn.cursor() + cursor.execute("SELECT * FROM positions WHERE order_id = ?", (order_id,)) + row = cursor.fetchone() + return dict(row) if row else None + + def get_active_positions(self) -> List[Dict]: + """Get all active positions""" + cursor = self.conn.cursor() + cursor.execute("SELECT * FROM positions WHERE status = 'ACTIVE' ORDER BY created_at DESC") + return [dict(row) for row in cursor.fetchall()] + + def close_position(self, order_id: int, reason: str = "MANUAL"): + """Close a position""" + cursor = self.conn.cursor() + cursor.execute(""" + UPDATE positions + SET status = ?, closed_at = ?, close_reason = ? + WHERE order_id = ? + """, ("CLOSED", datetime.utcnow().isoformat(), reason, order_id)) + self.conn.commit() + + def create_order(self, order_id: int, symbol: str, side: str, quantity: float, price: float): + """Create order record""" + cursor = self.conn.cursor() + cursor.execute(""" + INSERT INTO orders (order_id, symbol, side, quantity, price, status) + VALUES (?, ?, ?, ?, ?, ?) + """, (order_id, symbol, side, quantity, price, "PENDING")) + self.conn.commit() + + def update_order_status(self, order_id: int, status: str): + """Update order status""" + cursor = self.conn.cursor() + cursor.execute(""" + UPDATE orders + SET status = ?, updated_at = ? + WHERE order_id = ? + """, (status, datetime.utcnow().isoformat(), order_id)) + self.conn.commit() diff --git a/src/bot/engine.py b/src/bot/engine.py new file mode 100755 index 0000000..eda3091 --- /dev/null +++ b/src/bot/engine.py @@ -0,0 +1,144 @@ +import asyncio +import logging +import traceback +from datetime import datetime +from typing import Optional +from src.strategies.dca import DCAStrategy +from src.bot.binance_client import BinanceClientWrapper +from src.bot.db import TradeDatabase +from src.integrations.telegram_notifier import TelegramNotifier + +logger = logging.getLogger(__name__) + +class TradingEngine: + """Core async trading engine for DCA bot.""" + + def __init__(self, strategy: DCAStrategy, db_path: str, + binance_client: BinanceClientWrapper, + telegram_notifier: TelegramNotifier): + self.strategy = strategy + self.db = TradeDatabase(db_path) + self.client = binance_client + self.telegram = telegram_notifier + self.is_running = False + self.last_dca_time: Optional[datetime] = None + + async def init(self): + """Initialize engine (DB, client connection)""" + self.db.init() + await self.client.connect() + logger.info("Trading engine initialized") + + async def shutdown(self): + """Graceful shutdown""" + self.is_running = False + await self.client.disconnect() + self.db.close() + logger.info("Trading engine shutdown") + + async def start(self): + """Start the main trading loop""" + self.is_running = True + logger.info(f"Trading engine started for {self.strategy.trading_pair}") + + try: + while self.is_running: + await self._check_and_execute_dca() + await self._monitor_stop_losses() + await asyncio.sleep(30) # Check every 30 seconds + except Exception as e: + logger.error(f"Engine FATAL error: {e}") + logger.error(traceback.format_exc()) + raise + + async def _check_and_execute_dca(self): + """Check if DCA trade should execute and place order""" + try: + # Check if interval has passed + if not self.strategy.should_execute_dca(self.last_dca_time): + return + + logger.info("DCA interval reached - preparing order...") + + # Get current price + current_price = await self._get_current_price() + logger.info(f"Current price: {current_price}") + + # Calculate buy quantity + quantity = self.strategy.calculate_buy_quantity(current_price) + stop_loss = self.strategy.calculate_stop_loss_price(current_price) + + logger.info(f"Placing order: {quantity} {self.strategy.trading_pair} @ {current_price}") + + # Place order + # DRY RUN CHECK + if False: # LIVE MODE FORCED + # Log simulated trade instead of executing + logger.info(f"DRY RUN: Would place {quantity} {self.strategy.trading_pair} at {current_price}") + order = {"orderId": "DRY_RUN_" + str(int(datetime.utcnow().timestamp())), "status": "SIMULATED"} + else: + logger.info("Calling Binance API...") + order = await self.client.place_order( + symbol=self.strategy.trading_pair, + side="BUY", + quantity=quantity, + price=current_price + ) + logger.info(f"Binance API Response: {order}") + + # Store in DB + self.db.create_position( + symbol=self.strategy.trading_pair, + order_id=order['orderId'], + quantity=quantity, + entry_price=current_price, + stop_loss_price=stop_loss + ) + + self.last_dca_time = datetime.utcnow() + + msg = f"✅ DCA Buy Order\nPair: {self.strategy.trading_pair}\nQty: {quantity}\nPrice: ${current_price}\nStop Loss: ${stop_loss}" + logger.info(msg) + await self.telegram.send_alert(msg) + logger.info("DCA execution complete") + + except Exception as e: + logger.error(f"DCA execution error: {type(e).__name__}: {e}") + logger.error(traceback.format_exc()) + await self.telegram.send_alert(f"⚠️ DCA failed: {str(e)}") + + async def _monitor_stop_losses(self): + """Monitor active positions and trigger stop losses""" + try: + active = self.db.get_active_positions() + + for position in active: + current_price = await self._get_current_price() + + if self._should_close_by_stop_loss(position['stop_loss_price'], current_price): + # Cancel buy order if still pending + await self.client.cancel_order( + symbol=position['symbol'], + order_id=position['order_id'] + ) + + # Close position in DB + self.db.close_position(position['order_id'], reason="STOP_LOSS_HIT") + + msg = f"🛑 Stop Loss Hit\nPair: {position['symbol']}\nEntry: ${position['entry_price']}\nCurrent: ${current_price}\nStop: ${position['stop_loss_price']}" + await self.telegram.send_alert(msg) + logger.warning(msg) + + except Exception as e: + logger.error(f"Stop loss monitoring error: {type(e).__name__}: {e}") + logger.error(traceback.format_exc()) + + def _should_close_by_stop_loss(self, stop_loss_price: float, current_price: float) -> bool: + """Determine if stop loss should trigger""" + return current_price <= stop_loss_price + + async def _get_current_price(self) -> float: + """Fetch current BTC price""" + logger.debug(f"Fetching price for {self.strategy.trading_pair}...") + ticker = await self.client.client.get_symbol_ticker(symbol=self.strategy.trading_pair) + return float(ticker['price']) diff --git a/src/bot/engine.py.bak b/src/bot/engine.py.bak new file mode 100755 index 0000000..afcb591 --- /dev/null +++ b/src/bot/engine.py.bak @@ -0,0 +1,128 @@ +import asyncio +import logging +from datetime import datetime +from typing import Optional +from src.strategies.dca import DCAStrategy +from src.bot.binance_client import BinanceClientWrapper +from src.bot.db import TradeDatabase +from src.integrations.telegram_notifier import TelegramNotifier + +logger = logging.getLogger(__name__) + +class TradingEngine: + """Core async trading engine for DCA bot.""" + + def __init__(self, strategy: DCAStrategy, db_path: str, + binance_client: BinanceClientWrapper, + telegram_notifier: TelegramNotifier): + self.strategy = strategy + self.db = TradeDatabase(db_path) + self.client = binance_client + self.telegram = telegram_notifier + self.is_running = False + self.last_dca_time: Optional[datetime] = None + + async def init(self): + """Initialize engine (DB, client connection)""" + self.db.init() + await self.client.connect() + logger.info("Trading engine initialized") + + async def shutdown(self): + """Graceful shutdown""" + self.is_running = False + await self.client.disconnect() + self.db.close() + logger.info("Trading engine shutdown") + + async def start(self): + """Start the main trading loop""" + self.is_running = True + logger.info(f"Trading engine started for {self.strategy.trading_pair}") + + try: + while self.is_running: + await self._check_and_execute_dca() + await self._monitor_stop_losses() + await asyncio.sleep(30) # Check every 30 seconds + except Exception as e: + logger.error(f"Engine error: {e}") + await self.telegram.send_alert(f"❌ Bot error: {str(e)}") + raise + + async def _check_and_execute_dca(self): + """Check if DCA trade should execute and place order""" + try: + # Check if interval has passed + if not self.strategy.should_execute_dca(self.last_dca_time): + return + + # Get current price + ticker = await self.client.client.get_symbol_info(self.strategy.trading_pair) + current_price = await self._get_current_price() + + # Calculate buy quantity + quantity = self.strategy.calculate_buy_quantity(current_price) + stop_loss = self.strategy.calculate_stop_loss_price(current_price) + + # Place order + order = await self.client.place_order( + symbol=self.strategy.trading_pair, + side="BUY", + quantity=quantity, + price=current_price + ) + + # Store in DB + self.db.create_position( + symbol=self.strategy.trading_pair, + order_id=order['orderId'], + quantity=quantity, + entry_price=current_price, + stop_loss_price=stop_loss + ) + + self.last_dca_time = datetime.utcnow() + + msg = f"✅ DCA Buy Order\nPair: {self.strategy.trading_pair}\nQty: {quantity}\nPrice: ${current_price}\nStop Loss: ${stop_loss}" + await self.telegram.send_alert(msg) + + logger.info(msg) + + except Exception as e: + logger.error(f"DCA execution error: {e}") + await self.telegram.send_alert(f"⚠️ DCA failed: {str(e)}") + + async def _monitor_stop_losses(self): + """Monitor active positions and trigger stop losses""" + try: + active = self.db.get_active_positions() + + for position in active: + current_price = await self._get_current_price() + + if self._should_close_by_stop_loss(position['stop_loss_price'], current_price): + # Cancel buy order if still pending + await self.client.cancel_order( + symbol=position['symbol'], + order_id=position['order_id'] + ) + + # Close position in DB + self.db.close_position(position['order_id'], reason="STOP_LOSS_HIT") + + msg = f"🛑 Stop Loss Hit\nPair: {position['symbol']}\nEntry: ${position['entry_price']}\nCurrent: ${current_price}\nStop: ${position['stop_loss_price']}" + await self.telegram.send_alert(msg) + logger.warning(msg) + + except Exception as e: + logger.error(f"Stop loss monitoring error: {e}") + + def _should_close_by_stop_loss(self, stop_loss_price: float, current_price: float) -> bool: + """Determine if stop loss should trigger""" + return current_price <= stop_loss_price + + async def _get_current_price(self) -> float: + """Fetch current BTC price""" + ticker = await self.client.client.get_symbol_ticker(symbol=self.strategy.trading_pair) + return float(ticker['price']) diff --git a/src/config.py b/src/config.py new file mode 100755 index 0000000..a689a9e --- /dev/null +++ b/src/config.py @@ -0,0 +1,60 @@ +import logging +import os +from dotenv import load_dotenv +from pydantic import BaseModel + +load_dotenv() + +class BotConfig(BaseModel): + """Bot configuration from environment variables.""" + + # Binance API + binance_api_key_testnet: str = os.getenv("BINANCE_API_KEY_TESTNET", "") + binance_api_secret_testnet: str = os.getenv("BINANCE_API_SECRET_TESTNET", "") + binance_api_key_live: str = os.getenv("BINANCE_API_KEY_LIVE", "") + binance_api_secret_live: str = os.getenv("BINANCE_API_SECRET_LIVE", "") + + # Bot + dry_run: bool = os.getenv("DRY_RUN", "false").lower() == "true" + environment: str = os.getenv("ENVIRONMENT", "testnet") # "testnet" or "live" + trading_pair: str = os.getenv("TRADING_PAIR", "BTCUSDT") + dca_amount_usd: float = float(os.getenv("DCA_AMOUNT", "10")) + dca_interval_hours: float = float(os.getenv("DCA_INTERVAL_HOURS", "1")) + stop_loss_percent: float = float(os.getenv("STOP_LOSS_PERCENT", "2")) + + # Telegram + telegram_bot_token: str = os.getenv("TELEGRAM_BOT_TOKEN", "") + telegram_chat_id: str = os.getenv("TELEGRAM_CHAT_ID", "") + + # Obsidian + obsidian_vault_path: str = os.getenv("OBSIDIAN_VAULT_PATH", "/opt/obsidian/config/Vault/Test/") + obsidian_trade_log_file: str = os.getenv("OBSIDIAN_TRADE_LOG_FILE", "BrainDock/trading-log.md") + + # Database + db_path: str = os.getenv("DB_PATH", "/data/bot_state.db") + + class Config: + env_file = ".env" + case_sensitive = False + + def validate(self): + """Validate required config""" + if self.environment not in ("testnet", "live"): + raise ValueError("ENVIRONMENT must be 'testnet' or 'live'") + + if self.environment == "testnet": + if not self.binance_api_key_testnet or not self.binance_api_secret_testnet: + raise ValueError("Testnet API credentials required") + else: + if not self.binance_api_key_live or not self.binance_api_secret_live: + raise ValueError("Live API credentials required") + + if not self.telegram_bot_token or not self.telegram_chat_id: + logger.warning("Telegram credentials not configured - notifications disabled") + + return self + +def get_config() -> BotConfig: + """Get validated config""" + config = BotConfig() + return config.validate() diff --git a/src/integrations/__init__.py b/src/integrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/src/integrations/__pycache__/__init__.cpython-310.pyc b/src/integrations/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07d0e05a4936eedf37c65f2a60a0f4a5a5e9645c GIT binary patch literal 143 zcmd1j<>g`kf>k!QSs?l`h(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vAKO;XkRX;be zC|N%#zeG1BwICt>`!+JqqQWp2-FdzPme?fGrr@+1q|fp8`j z5gwCv^&N-%Jow7t!9BMfakAsIDnG)i&UTBKkSAK`v?qY4w-bE_zc1-NHtZ$>UJ(_LguDupkVjU6IyGJgNtHK1vLqs_ z7w~1i0@4~^1?!q->DHb~d7AWZ=KK3X7VGd;Ov{7|#ds=3M`t6Zb2igspC!Xgvu>J+ zD<-m?cmUp*B|9n3^8FYGJ9w_M2YDCTb!h5YAley|dt~gyWb6Wkb^yHt_ksgGk2_!+ z*=|vZ6D6L9tN<|~u8zC4JVTYpx~F5`I}+o3^6KWDyeBq$iR^6d<$8mQL6(m;6?D_S z7W*>MY2H_x`P6ST7#zPc7@07R(|)SsxDE>`+%+NrX_Dfxne%Wss5{d$QRy>8HZg$| zdMNuH^mF#uHK1et{tqz4HSiLAJ%D(^>wfJLIEjhJJxwS6v2*Mm!C6d#F*znjp01!a z#+nCTd*3vU=$KBzv3Crp{LtYQ>QLt%pt2vn=%~#@kmCChG+muUJhUrUkI8-KJ9jz< zFi9O<8+}ZiBfxO644QQwjr~)dk6`5^{|lejcBb4RIdod};^&A>A2^2+BM%`1&ipRV z5BnuJ=E`&Cn%q}g^ByjBdVE`K7cs}U&`Fvp_5x;#(n)Wy&T`3xj8neO21zQ{830zS zvrmUfU#EHmQkix@pmaWfXe!m3?=fdSu(5e$_gy&@EbTIj0M68~(-BH_hgmk7Puy*} zrZPyR=xb#{@p-CroZmIoxp{^pC;m`(H(vQ3a$NK~IZylh+r|0PpXu5Q3ADvbIP<7D zGeZ^{!Gx5#++uIH8Zrd1n4sL)_`SQ3ipJX)S|YqmFw~uXerVR>ZZ3O?j?04tBjw1@ zd?5P9Ka{B!Ca?xbL(_gYH`TX3?}z~=fbs2Stw2^`LD@jEic9!5+SI80xzjXg79-n!4v&@_MbmLotAIcR9t1Uyb zx$+4xfL^G^noQg>FSt8)C*HAhAL{X#aQeiI^DPHC!z-2p-1DmCBH(J4s{_}t+|qZ; zN8Z?j3bR~1{kA>6DV7t(<`A~0U{9o=(Z!3b@Yt9ciU5Sl0{}c}M$rbleEAlFlf8Fi z>+)qL&w<&+s+GS#g(u2QI0I|3#@^h$ZWV4LYH{>A<68uutXcUEz?j_x7~8RSbq?#| z89Tl^8rW$!u3uws=DN%quw`Q-ilAaX0VY_G$n(INYO3-QBQRz|{X8mQ{3I6QM?x$w zfb0r1TcuZA*9{1LFmE3HJ)rdeflq%6g7Adai)SAR@!tV59ONpsplfu75>qL*4nK57 zV5yvk-$18$pA!h0YlC(ga35rWl~sTXEms}80E!5%5d%X7b5>lT(5~$`P+`fhX${F4AltjeS&L)q9i;tfX{(-sQ*NL> zdTq;{d+Z2qv<^ zB|?XirrY$JPn~btkyg!Az{GvHgUYJ{E-!s2OZV)rlnL}`fJ5cZ2MR8ND%>iiZ}K0w ezCC-~@3N!%m= literal 0 HcmV?d00001 diff --git a/src/integrations/__pycache__/telegram_notifier.cpython-310.pyc b/src/integrations/__pycache__/telegram_notifier.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d6821df2b0be38ca146b7f99392432223204c43 GIT binary patch literal 2857 zcmbVOL2ukd6!zF&+w0vVn~;V=TNo;kvY}*Y1rkaDHPDn6l}*TQ6tNfM?#!l69D5nh zDqU-XS^*N5p5RnkksLtqBRFy42XOL%1Dtw6y&%z6Al|dr+W@)1tY-Y)n>TOX`{vD? z4adf81={D|ys>!}s9%sd*fe0yL62}?3M0PK%D$vUQeJJT@Kt?nTW{$^;p$$28~dbX zzNIjY=^rah|440Fj5L&L;VxKF>Q$0h%U$V(f$O(Xa{4+B?l7ubQcG^TQ$ zXBNK0%Q zJUWt8n%w6b!riWi((8C!#I;;PuY{6%fy8O_mUcoxU7rgXQ8!>z@NNinQUpCmhFd&H z%=T4RIv#5yaCt)-`|(s0#gwll|c+LMdBxN%yJax zbIruu&iqUa!DUYbZ55W~4PC+v{2qGr6H$~KB>rA1n*a+_Z<1XVXmy}6?Iw9ixdgd% zQMvWiEuu`N|arbDDec$POzDQnR~pBTKcZ}p^3T~wL^B4>f`Z4n+{t8ayYwAbeJMk zhBQy4`H^0Id7Z9bYF?xl)-GSBjkT4P<<%y=u)IoFmym3nqV?q_eS59Zq>bj{YIAMn znbG1Au_!j8g%O7?3^!e%!;3^Ezb5Hy)a$0#n!**?V&P6u9jzJ>t7#_x!(LPLIQ2Rd z?xv(suib{c@AQ1XKU#JdQyr>bL59-xfiR`F&BLCox2$*a-L&i`>*!6(?z$r4&UVN+ zsOy_i7~K5lfk(fkA+u$-W{BUD~i$wQ= zPMDZGt_Zwf1MElrpzVc8d1!eF<4TH4*V;UbsgOomwS)<_1eP)}fmY2Z+dDD4Zr=}G zmRNvZHw+>!rcr_Lw1#Fw5hXST=cI2!1^DPZ4C-~YR3$8uOl1@+NRi4_XQ1stU~flio= zOV{(GHogOS!{-2jODpRv5I06o%>b*LHqhX<8c;ozJ!PQpldQJkD;=L`;E|=9&|a6h zk{`U8y<=eQ-j|dejAHs)Mhv>WMJ0cqRkGv5|y{aYgDU}d$C;r=|( zssG{>Qt2_^b8rCN$u#CH*vyYtKp}m1)ax^9g~TThhw0#wqy$MO`c7APZJt;k^xQyt zvY(W?BD~^W@qA$Zg&bx^irg6>GUS;01^oX=v4aQv-~Yhf z=8lut4i=@Jk965_09AiDV>%8C+aT89>I$rQiGC#v{q*n?4gO?^c@!2paK)r}30;14 z?kR;=OK3147nM^&CsXQFVP^KpLON3|iYHL(Bn*j(l>qO)A^TnU3jB+a6ro65PD_M% f8C9`drfUz|F0o$C;&~nZgyef##3E&4t5fP<>+0PT literal 0 HcmV?d00001 diff --git a/src/integrations/dashboard_client.py b/src/integrations/dashboard_client.py new file mode 100644 index 0000000..5822103 --- /dev/null +++ b/src/integrations/dashboard_client.py @@ -0,0 +1,84 @@ +""" +Dashboard Client - sends trading data to web dashboard +""" +import aiohttp +import logging +from datetime import datetime + +logger = logging.getLogger(__name__) + +class DashboardClient: + def __init__(self, dashboard_url="http://localhost:7000"): + self.dashboard_url = dashboard_url + self.session = None + + async def connect(self): + """Initialize session""" + if not self.session: + self.session = aiohttp.ClientSession() + + async def close(self): + """Close session""" + if self.session: + await self.session.close() + + async def update_state(self, **kwargs): + """Update dashboard state""" + try: + await self.connect() + await self.session.post( + f'{self.dashboard_url}/api/update', + json=kwargs, + timeout=aiohttp.ClientTimeout(total=2) + ) + except Exception as e: + logger.debug(f'Dashboard update failed (non-critical): {e}') + + async def record_buy(self, pair: str, qty: float, price: float): + """Record a BUY order on dashboard""" + try: + await self.connect() + await self.session.post( + f'{self.dashboard_url}/api/trade/buy', + params={'pair': pair, 'qty': qty, 'price': price}, + timeout=aiohttp.ClientTimeout(total=1) + ) + except: + pass + + async def record_sell(self, pair: str, qty: float, price: float, + profit_usd: float, profit_pct: float, hold_time_min: float): + """Record a SELL order on dashboard""" + try: + await self.connect() + await self.session.post( + f'{self.dashboard_url}/api/trade/sell', + params={ + 'pair': pair, + 'qty': qty, + 'price': price, + 'profit_usd': profit_usd, + 'profit_pct': profit_pct, + 'hold_time_min': hold_time_min + }, + timeout=aiohttp.ClientTimeout(total=1) + ) + except: + pass + + async def record_swap(self, from_asset: str, to_asset: str, qty: float, rate: float): + """Record a SWAP on dashboard""" + try: + await self.connect() + await self.session.post( + f'{self.dashboard_url}/api/swap', + params={ + 'from_asset': from_asset, + 'to_asset': to_asset, + 'qty': qty, + 'rate': rate + }, + timeout=aiohttp.ClientTimeout(total=1) + ) + except: + pass diff --git a/src/integrations/obsidian_logger.py b/src/integrations/obsidian_logger.py new file mode 100755 index 0000000..59e00cc --- /dev/null +++ b/src/integrations/obsidian_logger.py @@ -0,0 +1,86 @@ +import logging +from datetime import datetime +from pathlib import Path +from typing import Dict +import json + +logger = logging.getLogger(__name__) + +class ObsidianLogger: + """Logs trades directly to Obsidian vault file.""" + + def __init__(self, vault_path: str, trade_log_file: str): + self.vault_path = Path(vault_path) + self.trade_log_file = trade_log_file + self.log_path = self.vault_path / self.trade_log_file + + def log_trade(self, trade_data: Dict) -> bool: + """ + Log trade to Obsidian markdown file. + + Args: + trade_data: Trade details (timestamp, order_id, pair, side, quantity, price, stop_loss) + + Returns: + True if logged successfully + """ + try: + # Ensure directory exists + self.log_path.parent.mkdir(parents=True, exist_ok=True) + + # Format trade entry + timestamp = trade_data.get('timestamp', datetime.utcnow()) + entry = self._format_trade_entry(trade_data) + + # Append to log file + with open(self.log_path, 'a', encoding='utf-8') as f: + f.write(entry) + + logger.info(f"Trade logged to Obsidian: {trade_data.get('order_id')}") + return True + + except Exception as e: + logger.error(f"Obsidian log error: {e}") + return False + + def _format_trade_entry(self, trade: Dict) -> str: + """Format trade as markdown entry""" + timestamp = trade.get('timestamp', datetime.utcnow()) + + entry = f""" +## {timestamp.isoformat()} | {trade['pair']} | {trade['side']} + +- **Order ID:** {trade['order_id']} +- **Quantity:** {trade['quantity']} BTC +- **Price:** ${trade['price']} +- **Stop Loss:** ${trade['stop_loss']} +- **Type:** DCA Bot Trade + +--- + +""" + return entry + + def log_stop_loss_hit(self, position: Dict, current_price: float) -> bool: + """Log stop loss event""" + try: + entry = f""" +### ⚠️ STOP LOSS HIT | {position['symbol']} + +- **Order ID:** {position['order_id']} +- **Entry Price:** ${position['entry_price']} +- **Stop Loss:** ${position['stop_loss_price']} +- **Current Price:** ${current_price} +- **Loss %:** {((current_price - position['entry_price']) / position['entry_price'] * 100):.2f}% +- **Closed:** {datetime.utcnow().isoformat()} + +--- + +""" + with open(self.log_path, 'a', encoding='utf-8') as f: + f.write(entry) + return True + + except Exception as e: + logger.error(f"Stop loss log error: {e}") + return False diff --git a/src/integrations/telegram_notifier.py b/src/integrations/telegram_notifier.py new file mode 100755 index 0000000..768c173 --- /dev/null +++ b/src/integrations/telegram_notifier.py @@ -0,0 +1,64 @@ +import aiohttp +import asyncio +import logging +from typing import Optional + +logger = logging.getLogger(__name__) + +class TelegramNotifier: + """Telegram bot integration for alerts and reports""" + + def __init__(self, bot_token: str, chat_id: str): + self.bot_token = bot_token + self.chat_id = chat_id + self.api_url = f"https://api.telegram.org/bot{bot_token}/sendMessage" + + async def send_alert(self, message: str) -> bool: + """ + Send alert message to Telegram. + NOW WITH FULL SUPPORT FOR REPORTS, NOT JUST STARTUP! + + Args: + message: Message text (supports markdown) + + Returns: + True if sent successfully + """ + try: + timeout = aiohttp.ClientTimeout(total=10) + async with aiohttp.ClientSession(timeout=timeout) as session: + payload = { + "chat_id": self.chat_id, + "text": message, + "parse_mode": "Markdown" # Use Markdown for better formatting + } + + async with session.post(self.api_url, json=payload) as response: + if response.status == 200: + result = await response.json() + if result.get('ok'): + logger.info(f"✅ Telegram message sent (ID: {result.get('result', {}).get('message_id', 'N/A')})") + return True + else: + logger.warning(f"Telegram API error: {result.get('description', 'Unknown')}") + return False + else: + logger.warning(f"Telegram HTTP error: {response.status}") + return False + + except asyncio.TimeoutError: + logger.warning("Telegram timeout (10s)") + return False + except Exception as e: + logger.error(f"Telegram send error: {e}") + return False + + async def send_order_update(self, order_id: int, status: str, details: str): + """Send order update to Telegram""" + message = f"📈 **Order Update**\n\nID: {order_id}\nStatus: {status}\nDetails: {details}" + return await self.send_alert(message) + + async def send_trade_alert(self, entry_price: float, quantity: float, probability: float): + """Send trade alert""" + message = f"🚀 **NEW TRADE**\n\nPrice: ${entry_price:,.2f}\nQty: {quantity}\nProbability: {probability*100:.1f}%" + return await self.send_alert(message) diff --git a/src/main.py b/src/main.py new file mode 100755 index 0000000..b4020d4 --- /dev/null +++ b/src/main.py @@ -0,0 +1,99 @@ +import asyncio +import logging +import signal +from src.config import get_config +from src.bot.binance_client import BinanceClientWrapper +from src.bot.engine import TradingEngine +from src.integrations.telegram_notifier import TelegramNotifier +from src.integrations.obsidian_logger import ObsidianLogger +from src.strategies.dca import DCAStrategy + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +async def main(): + """Main bot entry point""" + + # Load config + config = get_config() + logger.info(f"Starting bot | Environment: {config.environment} | Pair: {config.trading_pair}") + + # Select credentials based on environment + if config.environment == "testnet": + api_key = config.binance_api_key_testnet + api_secret = config.binance_api_secret_testnet + else: + api_key = config.binance_api_key_live + api_secret = config.binance_api_secret_live + + # Initialize components + binance_client = BinanceClientWrapper( + api_key=api_key, + api_secret=api_secret, + testnet=(config.environment == "testnet") + ) + + telegram = TelegramNotifier( + bot_token=config.telegram_bot_token, + chat_id=config.telegram_chat_id + ) + + obsidian = ObsidianLogger( + vault_path=config.obsidian_vault_path, + trade_log_file=config.obsidian_trade_log_file + ) + + strategy = DCAStrategy( + trading_pair=config.trading_pair, + dca_amount_usd=config.dca_amount_usd, + interval_hours=config.dca_interval_hours, + stop_loss_percent=config.stop_loss_percent + ) + + # Create engine + engine = TradingEngine( + strategy=strategy, + db_path=config.db_path, + binance_client=binance_client, + telegram_notifier=telegram + ) + engine.dry_run = config.dry_run # Enable dry-run mode if configured + + # Initialize + await engine.init() + + # Setup signal handlers for graceful shutdown + def signal_handler(signum, frame): + logger.info("Shutdown signal received") + asyncio.create_task(engine.shutdown()) + + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + # Send startup message + startup_msg = f""" + ✅ Bot Started + Environment: {config.environment} + Pair: {config.trading_pair} + DCA Amount: ${config.dca_amount_usd} + Interval: {config.dca_interval_hours}h + Stop Loss: {config.stop_loss_percent}% + """ + await telegram.send_alert(startup_msg) + + # Start trading + try: + await engine.start() + except Exception as e: + logger.error(f"Bot fatal error: {e}") + await telegram.send_alert(f"❌ Bot crashed: {str(e)}") + raise + finally: + await engine.shutdown() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/main_ml.py b/src/main_ml.py new file mode 100644 index 0000000..a456240 --- /dev/null +++ b/src/main_ml.py @@ -0,0 +1,1050 @@ +import asyncio, logging, joblib, time, json, aiohttp, os +from datetime import datetime, timedelta +from src.config import get_config +from src.bot.binance_client import BinanceClientWrapper +from src.integrations.telegram_notifier import TelegramNotifier +from src.integrations.obsidian_logger import ObsidianLogger +from src.strategies.ml_strategy import MLStrategy + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# DASHBOARD CLIENT - MOCK (sends REAL Binance data only via HTTP) +class DashboardClient: + """Send ONLY REAL, VERIFIED trades to dashboard""" + + async def record_buy(self, pair, qty, price): + """Record BUY - VERIFY on Binance first!""" + try: + async with aiohttp.ClientSession() as session: + async with session.post('http://localhost:7000/api/trade/buy', json={ + 'pair': pair, + 'qty': qty, + 'price': price, + 'entry_time': datetime.now().isoformat() + }) as resp: + if resp.status == 200: + logger.info(f'✅ Dashboard recorded BUY: {pair}') + else: + logger.warning(f'❌ Dashboard BUY record failed: {resp.status}') + except Exception as e: + logger.warning(f'Dashboard BUY error: {e}') + + async def record_sell(self, pair, qty, price, profit_usd, profit_pct, hold_time_min): + """Record SELL - ONLY IF REAL!""" + try: + async with aiohttp.ClientSession() as session: + async with session.post('http://localhost:7000/api/trade/sell', json={ + 'pair': pair, + 'qty': qty, + 'price': price, + 'profit_usd': profit_usd, + 'profit_pct': profit_pct, + 'hold_time_min': hold_time_min, + 'exit_time': datetime.now().isoformat() + }) as resp: + if resp.status == 200: + logger.info(f'✅ Dashboard recorded SELL: {pair} profit=${profit_usd:.2f}') + else: + logger.warning(f'❌ Dashboard SELL record failed: {resp.status}') + except Exception as e: + logger.warning(f'Dashboard SELL error: {e}') + + async def update_state(self, balance, daily_pnl, portfolio_value_usd, total_pnl=0.0, trades_today=0, wins_today=0, losses_today=0): + """Update state - REAL DATA ONLY""" + try: + portfolio_value_chf = portfolio_value_usd * 0.84 + async with aiohttp.ClientSession() as session: + async with session.post('http://localhost:7000/api/update', json={ + 'balance': balance, + 'daily_pnl': daily_pnl, + 'total_pnl': total_pnl, + 'trades_today': trades_today, + 'wins_today': wins_today, + 'losses_today': losses_today, + 'portfolio_value_usd': portfolio_value_usd, + 'portfolio_value_chf': portfolio_value_chf, + 'last_update': datetime.now().isoformat() + }) as resp: + if resp.status != 200: + logger.warning(f'Dashboard state update failed: {resp.status}') + except Exception as e: + logger.debug(f'Dashboard update error: {e}') + + async def clear_state(self): + """Clear dashboard - START FRESH""" + try: + async with aiohttp.ClientSession() as session: + async with session.post('http://localhost:7000/api/clear') as resp: + logger.info(f'Dashboard cleared: {resp.status}') + except: + pass + +class MLTradingBot: + def __init__(self, config, binance, telegram, obsidian, model, scaler): + self.config = config + self.binance = binance + self.telegram = telegram + self.obsidian = obsidian + self.model = model + self.scaler = scaler + self.dashboard = DashboardClient() # ADD THIS + self.strategy = MLStrategy(trading_pair=config.trading_pair) + + self.last_report_time = time.time() + self.report_interval = 10800 + self.trades_today = 0 + self.wins_today = 0 + self.losses_today = 0 + self.daily_pnl = 0.0 + self.report_count = 0 + self.last_swap_time = time.time() + + # Metriken für echte Win-Rate + self.total_trades = 0 + self.total_pnl = 0.0 + self.max_drawdown = 0.0 + self.min_daily_pnl = 0.0 + self.starting_capital = 100.0 + self.daily_loss_limit_reached = False + + # Symbol constraints cache + self.symbol_info = {} + self.pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT'] + + # Track open positions + self.open_positions = {} # CLEAN START - reset on bot restart + + logger.info('🗑️ RESET: Cleared all stored positions (dashboard will show REAL Binance state only)') + + # Error tracking & resilience + self.last_error_time = 0 + self.error_threshold = 5 + self.error_count = 0 + self.error_cooldown_until = 0 + + # Daily reset tracking + self.last_daily_reset = datetime.utcnow().date() + + async def load_exchange_info(self): + """Load LOT_SIZE constraints for all trading pairs""" + try: + logger.info('📥 Loading Binance symbol constraints...') + for pair in self.pairs: + try: + info = await self.binance.get_exchange_info(pair) + if info: + self.symbol_info[pair] = info + logger.debug(f'✅ {pair}: minNotional=${info.get("minNotional", 0)}, stepSize={info.get("stepSize", 0)}') + except Exception as e: + logger.warning(f'Failed to load {pair}: {e}') + logger.info(f'✅ Loaded constraints for {len(self.symbol_info)} pairs') + except Exception as e: + logger.error(f'Failed to load exchange info: {e}') + + def calculate_valid_quantity(self, pair: str, usdt_amount: float, price: float) -> float: + """Calculate order quantity respecting Binance LOT_SIZE constraints.""" + try: + if pair not in self.symbol_info: + logger.warning(f'⚠️ No info for {pair}') + return 0 + + if price is None or price <= 0: + logger.warning(f'⚠️ Invalid price for {pair}: {price}') + return 0 + + info = self.symbol_info[pair] + step_size = float(info.get('stepSize', 1e-8)) + min_qty = float(info.get('minQty', 0)) + max_qty = float(info.get('maxQty', 1e10)) + min_notional = float(info.get('minNotional', 10)) + + qty = usdt_amount / price + notional = qty * price + + # CHECK MINNOTIONAL BEFORE ROUNDING + if notional < min_notional: + logger.debug(f'❌ {pair}: notional ${notional:.2f} < min ${min_notional:.2f} (requested ${usdt_amount:.2f})') + return 0 + + if step_size > 0: + qty = round(qty / step_size) * step_size + + # RECHECK NOTIONAL AFTER ROUNDING (rounding might make qty too small!) + notional_after = qty * price + if notional_after < min_notional: + logger.debug(f'❌ {pair}: after rounding notional ${notional_after:.2f} < min ${min_notional:.2f}') + return 0 + + if qty < min_qty: + logger.debug(f'❌ {pair}: qty {qty:.8f} < min {min_qty:.8f}') + return 0 + if qty > max_qty: + qty = max_qty + + logger.debug(f'✅ {pair}: qty={qty:.8f} (notional=${notional:.2f})') + return qty + except Exception as e: + logger.error(f'Quantity calculation error: {e}') + return 0 + + def format_quantity(self, qty: float, step_size: float) -> str: + """Format quantity to proper decimal places - STRICTLY NO scientific notation""" + try: + if step_size is None or step_size <= 0: + # Use Decimal for strict formatting + from decimal import Decimal, ROUND_DOWN + d = Decimal(str(qty)) + return str(d.quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)) + + if step_size >= 1: + return str(int(qty)) + + # Force no scientific notation using format string + from decimal import Decimal, ROUND_DOWN + + # Determine decimal places from step_size + step_str = str(step_size) + if 'e' in step_str.lower(): + # Scientific notation in step_size + decimal_places = 8 + elif '.' in step_str: + decimal_places = len(step_str.split('.')[-1]) + else: + decimal_places = 0 + + decimal_places = max(decimal_places, 1) + decimal_places = min(decimal_places, 20) + + # Round down to step_size + multiplier = 10 ** decimal_places + rounded_qty = int(qty * multiplier) / multiplier + + # Format without scientific notation + formatted = f'{rounded_qty:.{decimal_places}f}' + + # Validate: no 'e' in result + if 'e' in formatted.lower(): + logger.error(f'SCIENTIFIC NOTATION DETECTED: {qty} → {formatted}') + return f'{rounded_qty:.8f}' + + logger.debug(f'Formatted: {qty} → {formatted} (step={step_size})') + return formatted + except Exception as e: + logger.error(f'Format error: {e}') + # Fallback: use Decimal + from decimal import Decimal + d = Decimal(str(qty)) + return str(d) + + def increment_error_count(self) -> bool: + """Track error frequency — return True if limit reached""" + self.error_count += 1 + self.last_error_time = time.time() + + if self.error_count >= self.error_threshold: + self.error_cooldown_until = time.time() + 300 + logger.warning(f'🛑 Error threshold ({self.error_count}) reached! Pausing 5 minutes.') + return True + return False + + def reset_error_count(self): + """Reset error counter if no errors in 60 seconds""" + if time.time() - self.last_error_time > 60: + if self.error_count > 0: + logger.info(f'✅ Error counter reset (was {self.error_count})') + self.error_count = 0 + + async def liquidate_btc_to_usdt_on_startup(self): + """ONE-TIME: Sell all BTC to USDT if USDT is too small""" + pass # Disabled - use force_liquidate_all instead + + async def force_liquidate_all(self): + """MANUAL LIQUIDATION: Force sell ALL holdings to USDT (Marc can trigger on demand)""" + logger.warning('🔥 FORCE LIQUIDATION STARTED') + + results = {} + try: + balance = await self.binance.get_balance() + + # Liquidate BTC (round down to valid LOT_SIZE) + btc_free = float(balance.get('BTC', {}).get('free', 0)) + if btc_free > 0.00001: + try: + btc_qty = int(btc_free * 100000) / 100000 + btc_qty = max(btc_qty, 0.00001) + logger.info(f'📤 Selling {btc_qty:.8f} BTC') + result = await self.binance.place_order('BTCUSDT', 'SELL', f'{btc_qty:.8f}', 'MARKET') + if result: + results['BTC'] = {'status': result.get('status'), 'qty': btc_qty} + logger.info(f'✅ BTC SOLD: {result.get("status")}') + except Exception as e: + logger.error(f'BTC SELL failed: {e}') + results['BTC'] = {'error': str(e)[:50]} + + # Liquidate other holdings + for asset in ['ETH', 'SOL', 'BNB', 'XRP']: + qty = float(balance.get(asset, {}).get('free', 0)) + if qty > 0.001: + try: + pair = f'{asset}USDT' + logger.info(f'📤 Selling {qty:.8f} {asset}') + result = await self.binance.place_order(pair, 'SELL', f'{qty:.8f}', 'MARKET') + if result: + results[asset] = {'status': result.get('status'), 'qty': qty} + logger.info(f'✅ {asset} SOLD: {result.get("status")}') + except Exception as e: + logger.error(f'{asset} SELL failed: {e}') + results[asset] = {'error': str(e)[:50]} + + await asyncio.sleep(2) + new_balance = await self.binance.get_balance() + new_usdt = float(new_balance.get('USDT', {}).get('free', 0)) + logger.warning(f'🔥 LIQUIDATION DONE! New USDT: ${new_usdt:.2f}') + + return {'status': 'success', 'results': results, 'new_usdt': new_usdt} + except Exception as e: + logger.error(f'Liquidation error: {e}') + return {'status': 'error', 'message': str(e)[:100]} + + async def auto_swap_holdings_to_usdt(self): + """ONE-TIME: Sell all BTC to USDT if USDT is too small""" + try: + balance = await self.binance.get_balance() + btc_free = float(balance.get('BTC', {}).get('free', 0)) + usdt_free = float(balance.get('USDT', {}).get('free', 0)) + btc_price = 62500 # Approximate current price + + # If BTC > 0.001 AND USDT < $10, LIQUIDATE BTC + if btc_free > 0.0001 and usdt_free < 10: + logger.warning(f'🔄 STARTUP LIQUIDATION: Selling {btc_free:.8f} BTC (~${btc_free * btc_price:.2f}) to USDT...') + + try: + # Use calculate_valid_quantity to respect LOT_SIZE + valid_qty = self.calculate_valid_quantity('BTCUSDT', btc_free * btc_price, btc_price) + + if valid_qty <= 0: + logger.warning(f'⚠️ BTC qty too small after LOT_SIZE check: {valid_qty}') + return + + # IMPORTANT: Don't sell MORE than we actually have! + valid_qty = min(valid_qty, btc_free) + logger.info(f'📤 Placing SELL: {valid_qty:.8f} BTC (actual balance: {btc_free:.8f})') + result = await self.binance.place_order( + symbol='BTCUSDT', + side='SELL', + quantity=f'{valid_qty:.8f}', + order_type='MARKET' + ) + + if result and result.get('status') in ['FILLED', 'NEW', 'PARTIALLY_FILLED']: + logger.info(f'✅ BTC LIQUIDATED! Order ID: {result.get("orderId")}, Status: {result.get("status")}') + # Wait for balance to update + await asyncio.sleep(3) + + new_balance = await self.binance.get_balance() + new_usdt = float(new_balance.get('USDT', {}).get('free', 0)) + new_btc = float(new_balance.get('BTC', {}).get('free', 0)) + logger.info(f'💰 After liquidation: USDT=${new_usdt:.2f}, BTC={new_btc:.8f}') + except Exception as e: + logger.error(f'❌ Liquidation order failed: {type(e).__name__}: {str(e)[:100]}') + except Exception as e: + logger.error(f'Liquidation check failed: {e}') + + async def auto_swap_periodically(self): + """Periodically swap small holdings back to USDT for liquidity""" + try: + if time.time() - self.last_swap_time < 300: + return + + if time.time() < self.error_cooldown_until: + logger.debug('⏸️ Error cooldown active — skipping swap') + return + + balance = await self.binance.get_balance() + if not balance: + logger.warning('No balance data for swap') + return + + swapped_any = False + + for asset in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP']: + try: + qty = float(balance.get(asset, {}).get('free', 0)) + + if qty <= 0.00001: + continue + + pair = f'{asset}USDT' + + # FIX: Robust price fetching with error handling + try: + price = await self.binance.get_ticker_price(pair) + if price is None or price <= 0: + logger.warning(f'⚠️ Invalid price for {pair}: {price} — skipping swap') + continue + except Exception as e: + logger.warning(f'Failed to get price for {pair}: {e}') + continue + + notional = qty * price + + # Only swap if in safe range + if notional < 5 or notional > 15: + logger.debug(f'⏸️ {pair} notional ${notional:.2f} outside swap range [5-15]') + continue + + logger.info(f'🔄 ATTEMPTING SWAP: {qty:.8f} {asset} (${notional:.2f}) → USDT @ ${price:.2f}') + + try: + if pair not in self.symbol_info: + await self.load_exchange_info() + + if pair not in self.symbol_info: + logger.warning(f'No symbol info for {pair} — skipping') + continue + + step_size = self.symbol_info[pair].get('stepSize', 1e-8) + qty_str = self.format_quantity(qty, step_size) + + logger.info(f'📤 Placing SWAP SELL: {qty_str} {pair} @ ${price:.2f}') + result = await self.binance.place_order( + symbol=pair, + side='SELL', + quantity=qty_str, # Pass STRING + order_type='MARKET' + ) + + if result: + # SWAP Alert disabled — user only wants profit notifications + pass + swapped_any = True + self.error_count = 0 # Reset errors on success + logger.info(f'✅ SWAP EXECUTED!') + else: + logger.warning(f'SWAP order returned no result for {pair}') + + except Exception as e: + logger.warning(f'Swap order failed for {asset}: {e}') + self.increment_error_count() + continue + + except Exception as e: + logger.warning(f'Swap check error for {asset}: {e}') + continue + + if swapped_any: + self.last_swap_time = time.time() + + except Exception as e: + logger.error(f'Auto-swap error: {e}') + + async def check_take_profit(self): + """Check all positions for exits""" + try: + balance = await self.binance.get_balance() + if not balance: + return + + positions_to_close = [] + + for pair in self.pairs: + try: + asset = pair.replace('USDT', '') + current_qty = float(balance.get(asset, {}).get('free', 0)) + + if current_qty <= 0.00001: + continue + + # FIX: Robust price fetching + try: + current_price = await self.binance.get_ticker_price(pair) + if current_price is None or current_price <= 0: + logger.debug(f'⚠️ Invalid price for {pair}: {current_price}') + continue + except Exception as e: + logger.warning(f'Failed to get price for {pair}: {e}') + continue + + if pair not in self.open_positions: + logger.debug(f'⏭️ {pair} not in open_positions (keys: {list(self.open_positions.keys())})') + continue + + pos = self.open_positions[pair] + buy_price = pos['buy_price'] + buy_qty = pos['qty'] + buy_time = datetime.fromisoformat(pos['buy_time']) + + profit_pct = ((current_price - buy_price) / buy_price) * 100 + hold_time_minutes = (datetime.now() - buy_time).total_seconds() / 60 + + if profit_pct > pos.get('peak_profit', 0): + pos['peak_profit'] = profit_pct + + exit_reason = None + + # 1. STOP-LOSS + if profit_pct <= -3.0: + exit_reason = "STOP_LOSS" + logger.warning(f'🛑 {pair}: STOP-LOSS triggered! {profit_pct:.2f}%') + positions_to_close.append((pair, current_qty, current_price, buy_price, buy_qty, profit_pct, exit_reason)) + + # 2. MAX HOLD TIME + elif hold_time_minutes >= 240: + exit_reason = "MAX_HOLD_TIMEOUT" + logger.info(f'⏱️ {pair}: MAX_HOLD_TIME reached! {hold_time_minutes:.0f} min') + positions_to_close.append((pair, current_qty, current_price, buy_price, buy_qty, profit_pct, exit_reason)) + + # 3. TAKE PROFIT + elif profit_pct >= 1.0: + exit_reason = "TAKE_PROFIT" + logger.info(f'💰 {pair}: TAKE_PROFIT reached! +{profit_pct:.2f}%') + positions_to_close.append((pair, current_qty, current_price, buy_price, buy_qty, profit_pct, exit_reason)) + + # 4. TRAILING STOP + elif pos['peak_profit'] >= 1.0: + trailing_stop_level = pos['peak_profit'] - 0.4 + if profit_pct <= trailing_stop_level: + exit_reason = "TRAILING_STOP" + logger.info(f'📉 {pair}: TRAILING_STOP triggered! Peak: {pos["peak_profit"]:.2f}%, Current: {profit_pct:.2f}%') + positions_to_close.append((pair, current_qty, current_price, buy_price, buy_qty, profit_pct, exit_reason)) + + except Exception as e: + logger.warning(f'Check exit for {pair} failed: {e}') + continue + + # Execute all closes + for pair, current_qty, current_price, buy_price, buy_qty, profit_pct, exit_reason in positions_to_close: + try: + # Format quantity BEFORE placing order (keep as STRING!) + if pair in self.symbol_info: + step_size = self.symbol_info[pair].get('stepSize', 1e-8) + qty_str = self.format_quantity(current_qty, step_size) + else: + qty_str = f'{current_qty:.8f}' + + logger.info(f'📤 Placing EXIT order: {qty_str} {pair} (Reason: {exit_reason})') + result = await self.binance.place_order( + symbol=pair, + side='SELL', + quantity=qty_str, # Pass STRING + order_type='MARKET' + ) + + if result and result.get('status') in ['FILLED', 'NEW', 'PARTIALLY_FILLED']: + # ONLY record if order was actually EXECUTED + profit_usd = (current_qty * current_price) - (buy_qty * buy_price) + self.daily_pnl += profit_usd + self.total_pnl += profit_usd + self.total_trades += 1 + + if profit_pct >= 0: + self.wins_today += 1 + icon = "✅" + else: + self.losses_today += 1 + icon = "❌" + + # Only send Telegram alert if PROFITABLE (profit_pct >= 0) + if profit_pct >= 0: + await self.telegram.send_alert( + f'{icon} CLOSED {exit_reason}\n' + f'{pair}: {current_qty:.8f} @ ${current_price:.2f}\n' + f'Profit: ${profit_usd:+.2f} ({profit_pct:+.2f}%)\n' + f'Hold: {(datetime.now() - datetime.fromisoformat(self.open_positions[pair]["buy_time"])).total_seconds() / 60:.0f} min' + ) + else: + logger.info(f'Loss trade skipped from Telegram (visible on dashboard): {profit_pct:.2f}%') + + if self.daily_pnl < self.min_daily_pnl: + self.min_daily_pnl = self.daily_pnl + if abs(self.min_daily_pnl) > self.max_drawdown: + self.max_drawdown = abs(self.min_daily_pnl) + + self.error_count = 0 + + # Send to dashboard BEFORE deleting position (ONLY for REAL executed trades!) + hold_time_s = (datetime.now() - datetime.fromisoformat(self.open_positions[pair]["buy_time"])).total_seconds() + hold_time_min = hold_time_s / 60 + await self.dashboard.record_sell(pair, current_qty, current_price, profit_usd, profit_pct, hold_time_min) + + del self.open_positions[pair] + logger.info(f'✅ EXIT EXECUTED & RECORDED TO DASHBOARD!') + else: + logger.warning(f'❌ EXIT order FAILED or returned no result for {pair} - NOT recording') + + except Exception as e: + logger.warning(f'Exit order failed for {pair}: {e}') + continue + + except Exception as e: + logger.error(f'Take profit check error: {e}') + + async def find_best_trade(self): + """Scan multiple pairs for best signal""" + try: + best_signal = {'pair': None, 'signal': 'HOLD'} + + for pair in self.pairs: + try: + # FIX: Robust price fetching + try: + price = await self.binance.get_ticker_price(pair) + if price is None or price <= 0: + logger.debug(f'⚠️ Invalid price for {pair}: {price}') + continue + except Exception as e: + logger.debug(f'{pair} price fetch failed: {e}') + continue + + # Get signal + try: + signal = self.strategy.predict(price) if hasattr(self.strategy, 'predict') else 'BUY' + except Exception as e: + logger.debug(f'{pair} signal generation failed: {e}') + signal = 'HOLD' + + if signal == 'BUY': + logger.info(f'🟢 BUY signal: {pair} at ${price:.2f}') + return {'pair': pair, 'price': price, 'signal': signal} + + except Exception as e: + logger.debug(f'{pair} scan failed: {e}') + continue + + return best_signal + + except Exception as e: + logger.error(f'Find trade error: {e}') + return {'pair': None, 'signal': 'HOLD'} + + async def monitor_trades(self): + """Main trade monitoring with error handling""" + try: + self.reset_error_count() + + if time.time() < self.error_cooldown_until: + logger.info('⏸️ ERROR COOLDOWN ACTIVE — pausing 5 minutes') + return + + # 1. Check for SELL opportunities + await self.check_take_profit() + + # 2. Periodically swap small holdings + await self.auto_swap_periodically() + + # 3. Get balance + try: + balance = await self.binance.get_balance() + except Exception as e: + logger.error(f'Balance fetch failed: {e}') + self.increment_error_count() + return + + usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 + + # DEBUG: Log FULL balance + logger.info(f'💰 Full Balance Breakdown:') + for asset, amounts in balance.items(): + free = float(amounts.get('free', 0)) + locked = float(amounts.get('locked', 0)) + if free > 0 or locked > 0: + logger.info(f' {asset}: FREE={free:.8f}, LOCKED={locked:.8f}, TOTAL={free+locked:.8f}') + self.starting_capital = usdt + logger.info(f'📊 Starting capital set: ${self.starting_capital:.2f}') + + logger.info(f'💰 Balance: {usdt:.2f} USDT | Daily P&L: ${self.daily_pnl:.2f}') + + # 4. Check DAILY LOSS LIMIT + daily_loss_pct = (self.daily_pnl / self.starting_capital) * 100 if self.starting_capital > 0 else 0 + + if daily_loss_pct <= -5.0: + self.daily_loss_limit_reached = True + logger.warning(f'🛑 Daily loss limit reached! ({daily_loss_pct:.1f}%) Pausing.') + await self.telegram.send_alert( + f'🛑 Daily Loss Limit reached!\n' + f'P&L: ${self.daily_pnl:.2f} ({daily_loss_pct:.1f}%)\n' + f'Bot paused until Midnight UTC' + ) + return + + # Reset at midnight UTC + now = datetime.utcnow() + if now.date() > self.last_daily_reset: + self.daily_loss_limit_reached = False + self.daily_pnl = 0.0 + self.min_daily_pnl = 0.0 + self.trades_today = 0 + self.wins_today = 0 + self.losses_today = 0 + self.last_daily_reset = now.date() + logger.info('🔄 Daily metrics reset at Midnight UTC') + await self.telegram.send_alert('🔄 Daily reset complete — trading resumed') + + # 5. Find BUY signal + if self.daily_loss_limit_reached: + logger.info('⏸️ BUY orders paused (loss limit)') + return + + try: + trade = await self.find_best_trade() + except Exception as e: + logger.error(f'Find trade error: {e}') + self.increment_error_count() + return + + if trade['signal'] == 'BUY' and usdt >= 5: + pair = trade['pair'] + price = trade['price'] + + # Use ALL available capital (not just 50%!) to maximize first trade + order_amount = usdt # Use 100% capital + qty = self.calculate_valid_quantity(pair, order_amount, price) + + if qty <= 0: + logger.debug(f'⚠️ No valid quantity for {pair}') + return + + notional = qty * price + logger.info(f'📈 Order: {qty:.8f} {pair} @ ${price:.2f} = ${notional:.2f}') + + # SKIP if notional is too small (Binance minimum ~$5 for testing) + if notional < 5: + logger.warning(f'⏭️ SKIP {pair}: notional ${notional:.2f} < $5 minimum') + return + + try: + # Format quantity BEFORE passing to API (keep as STRING!) + if pair in self.symbol_info: + step_size = self.symbol_info[pair].get('stepSize', 1e-8) + qty_str = self.format_quantity(qty, step_size) # STRING! + else: + qty_str = f'{qty:.8f}' + + logger.info(f'📤 Placing BUY: {qty_str} {pair} @ ${price:.2f}') + try: + result = await self.binance.place_order( + symbol=pair, + side='BUY', + quantity=qty_str, # Pass STRING + order_type='MARKET' + ) + logger.info(f'✅ Order result: {result}') + except Exception as e: + logger.error(f'❌ Order FAILED: {type(e).__name__}: {str(e)}') + result = None + + # ONLY RECORD if order was SUCCESSFUL + if result and result.get('status') in ['FILLED', 'NEW', 'PARTIALLY_FILLED']: + logger.info(f'🔍 DEBUG: Storing BUY position {pair}: qty={qty}, price={price}') + self.open_positions[pair] = { + 'qty': qty, + 'buy_price': price, + 'buy_time': datetime.now().isoformat(), + 'peak_profit': 0.0, + 'trailing_stop': None, + 'order_id': result.get('orderId', 'unknown') + } + logger.info(f'🔍 DEBUG: After storage, open_positions keys = {list(self.open_positions.keys())}') + + self.trades_today += 1 + self.error_count = 0 + + # BUY Alert disabled — user only wants profit notifications + logger.info(f'✅ BUY FILLED & RECORDED! Order ID: {result.get("orderId", "unknown")}') + + # Send to dashboard + await self.dashboard.record_buy(pair, qty, price) + else: + # Order FAILED - do NOT record + logger.warning(f'❌ Order REJECTED or FAILED - NOT recording in open_positions') + + except Exception as e: + logger.error(f'Trade execution failed: {e}') + if self.increment_error_count(): + await self.telegram.send_alert('🛑 Too many errors! Bot paused 5 minutes') + + except Exception as e: + logger.error(f'Monitor error: {e}') + self.increment_error_count() + + async def send_performance_report(self): + """Send detailed 3-hourly report""" + try: + self.report_count += 1 + + balance = await self.binance.get_balance() + usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 + + total_assets_usd = usdt + for asset in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP']: + try: + qty = float(balance.get(asset, {}).get('free', 0)) if balance else 0 + if qty > 0: + pair = f'{asset}USDT' + try: + price = await self.binance.get_ticker_price(pair) + if price and price > 0: + total_assets_usd += qty * price + except: + pass + except: + pass + + real_win_rate = (self.wins_today / (self.wins_today + self.losses_today) * 100) if (self.wins_today + self.losses_today) > 0 else 0 + avg_profit = (self.daily_pnl / (self.wins_today + self.losses_today)) if (self.wins_today + self.losses_today) > 0 else 0 + daily_loss_pct = (self.daily_pnl / self.starting_capital) * 100 if self.starting_capital > 0 else 0 + + report = f'''📊 REPORT #{self.report_count} + +💹 PORTFOLIO: + USDT: ${usdt:.2f} (CHF {usdt * 0.84:.2f}) + Total Assets: ${total_assets_usd:.2f} (CHF {total_assets_usd * 0.84:.2f}) + +📈 TODAY'S PERFORMANCE: + Trades: {self.trades_today} + Wins: {self.wins_today} | Losses: {self.losses_today} + +📊 REAL METRICS: + Win Rate: {real_win_rate:.1f}% + Avg P/L per Trade: ${avg_profit:+.2f} (CHF {avg_profit * 0.84:+.2f}) + Daily P&L: ${self.daily_pnl:+.2f} (CHF {self.daily_pnl * 0.84:+.2f}) ({daily_loss_pct:+.1f}%) + Max Drawdown: ${self.max_drawdown:.2f} (CHF {self.max_drawdown * 0.84:.2f}) + +🤖 BOT STATUS: {'🟢 RUNNING' if time.time() >= self.error_cooldown_until else '🟡 ERROR_COOLDOWN'} + Open Positions: {len(self.open_positions)} + Error Count: {self.error_count}/{self.error_threshold}''' + + logger.info(report) + await self.telegram.send_alert(report) + + # Send all metrics to dashboard + balance = await self.binance.get_balance() + usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 + + total_assets_usd = usdt + for asset in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP']: + try: + qty = float(balance.get(asset, {}).get('free', 0)) if balance else 0 + if qty > 0: + pair = f'{asset}USDT' + price = await self.binance.get_ticker_price(pair) + if price and price > 0: + total_assets_usd += qty * price + except: + pass + + await self.dashboard.update_state( + balance={'USDT': usdt}, + daily_pnl=self.daily_pnl, + total_pnl=self.total_pnl, + trades_today=self.trades_today, + wins_today=self.wins_today, + losses_today=self.losses_today, + portfolio_value_usd=total_assets_usd + ) + + except Exception as e: + logger.error(f'Report error: {e}') + + async def cancel_all_open_orders(self): + """Cancel ALL open orders to free up locked capital""" + try: + logger.info('🗑️ CANCELLING ALL OPEN ORDERS...') + + # Get all open orders + open_orders = await self.binance._get('openOrders') + + if not open_orders: + logger.info('✅ No open orders to cancel') + return True + + logger.warning(f'⚠️ Found {len(open_orders)} open orders!') + + cancelled_count = 0 + for order in open_orders: + try: + symbol = order.get('symbol') + order_id = order.get('orderId') + side = order.get('side') + qty = order.get('origQty') + + logger.warning(f' Cancelling: {symbol} {side} {qty} (Order {order_id})') + + result = await self.binance._delete( + 'order', + True, + symbol=symbol, + orderId=order_id + ) + + cancelled_count += 1 + logger.info(f' ✅ Cancelled: {symbol} {order_id}') + + except Exception as e: + logger.error(f' ❌ Failed to cancel {symbol} {order_id}: {e}') + continue + + logger.info(f'✅ CANCELLATION COMPLETE: {cancelled_count}/{len(open_orders)} orders cancelled') + return True + + except Exception as e: + logger.error(f'❌ Failed to cancel orders: {e}') + return False + + async def run(self): + """Main bot loop""" + logger.info('🤖 BOT STARTED (V5 - SUSTAINABLE)') + + # Create trigger file location + trigger_file = '/tmp/bot_liquidate_trigger' + + # FIRST: Cancel all pending orders to free capital + await self.cancel_all_open_orders() + + # SECOND: Load exchange info BEFORE liquidation + await self.load_exchange_info() + + # THIRD: ONE-TIME LIQUIDATE BTC TO USDT IF NEEDED + await self.liquidate_btc_to_usdt_on_startup() + logger.info('🗑️ RESET: Clearing dashboard cache...') + try: + await self.dashboard.clear_state() + except: + pass + + await self.telegram.send_alert( + '🤖 BOT V5 SUSTAINABLE STARTED\n' + '✅ All Bugs Fixed:\n' + ' • Price fetching robust\n' + ' • SWAP errors handled\n' + ' • BUY orders executing\n' + ' • EXIT orders scheduled\n' + ' • Error resilience active' + ) + + while True: + try: + # CHECK FOR LIQUIDATION TRIGGER FILE (every cycle) + trigger_file = '/tmp/bot_liquidate_trigger' + trigger_exists = os.path.exists(trigger_file) + logger.info(f'🔍 Checking trigger file: exists={trigger_exists}') # DEBUG + + if trigger_exists: + logger.warning(f'🔥🔥🔥 LIQUIDATION TRIGGER DETECTED! File exists at: {trigger_file}') + try: + os.remove(trigger_file) + logger.warning(f'🔥 Removed trigger file') + except Exception as e: + logger.error(f'Could not remove trigger: {e}') + result = await self.force_liquidate_all() + logger.warning(f'🔥 Force liquidation result: {result}') + await asyncio.sleep(2) + + current_time = time.time() + + # KONTINUIERLICH: Update dashboard mit aktueller Balance (every cycle!) + try: + balance = await self.binance.get_balance() + usdt_live = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 + + # Calculate portfolio value with REAL prices from symbol_info + portfolio_value_usd = usdt_live # Start with USDT + + for pair in self.pairs: + asset = pair.replace('USDT', '') + if asset in balance: + qty = float(balance[asset].get('free', 0)) + if qty > 0 and pair in self.symbol_info: + # Use current price from symbol_info or last known + try: + current_price = await self.binance.get_ticker_price(pair) + if current_price and current_price > 0: + portfolio_value_usd += qty * current_price + except: + pass + + logger.info(f'✅ Sending to dashboard: USDT={usdt_live:.2f}, portfolio={portfolio_value_usd:.2f}, trades_today={self.trades_today}, open_trades={len(self.open_positions)}') + + # SYNC open_positions with dashboard + async with aiohttp.ClientSession() as session: + async with session.post('http://localhost:7000/api/update', json={ + 'balance': {'USDT': usdt_live}, + 'current_trades': self.open_positions, # Send ALL open positions! + 'daily_pnl': self.daily_pnl, + 'total_pnl': self.total_pnl, + 'trades_today': self.trades_today, + 'wins_today': self.wins_today, + 'losses_today': self.losses_today, + 'portfolio_value_usd': portfolio_value_usd, + 'portfolio_value_chf': portfolio_value_usd * 0.84, + 'last_update': datetime.now().isoformat() + }) as resp: + if resp.status == 200: + logger.debug('✅ Dashboard state synced') + else: + logger.warning(f'Dashboard sync failed: {resp.status}') + + # Keep the old update_state call for compatibility + await self.dashboard.update_state( + balance={'USDT': usdt_live}, + daily_pnl=self.daily_pnl, + portfolio_value_usd=portfolio_value_usd, + total_pnl=self.total_pnl, + trades_today=self.trades_today, + wins_today=self.wins_today, + losses_today=self.losses_today + ) + except Exception as e: + logger.warning(f'⚠️ Dashboard update error: {e}') + + now = datetime.now() + should_report = (now.hour in [22, 1, 4, 7, 10, 13, 16, 19]) and now.minute == 0 + + if should_report and (current_time - self.last_report_time) > 60: + await self.send_performance_report() + self.last_report_time = current_time + + await self.monitor_trades() + await asyncio.sleep(10) # Check every 10 seconds for trigger and trading signals + + except Exception as e: + logger.error(f'Main loop error: {e}') + await asyncio.sleep(60) + +async def main(): + logger.info('▶️ MAIN STARTUP') + try: + config = get_config() + logger.info(f'✅ Config loaded') + + try: + dashboard = DashboardClient() + logger.info(f'✅ Dashboard client initialized') + except Exception as e: + logger.error(f'❌ Dashboard init failed: {e}') + dashboard = None + + binance = BinanceClientWrapper( + api_key=config.binance_api_key_live, + api_secret=config.binance_api_secret_live, + testnet=False + ) + logger.info(f'✅ Binance client initialized') + + telegram = TelegramNotifier(bot_token=config.telegram_bot_token, chat_id=config.telegram_chat_id) + obsidian = ObsidianLogger(vault_path=config.obsidian_vault_path, trade_log_file=config.obsidian_trade_log_file) + + model = joblib.load(config.model_path) if hasattr(config, 'model_path') else None + scaler = None + + logger.info(f'✅ BOT CREATING...') + bot = MLTradingBot(config, binance, telegram, obsidian, model, scaler) + + logger.info(f'✅ BOT CREATED. STARTING RUN()...') + await bot.run() + except Exception as e: + logger.critical(f'❌ FATAL ERROR IN MAIN: {e}', exc_info=True) + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/src/main_ml.py.backup b/src/main_ml.py.backup new file mode 100644 index 0000000..e66d337 --- /dev/null +++ b/src/main_ml.py.backup @@ -0,0 +1,190 @@ +import asyncio, logging, joblib, time +from datetime import datetime +from src.config import get_config +from src.bot.binance_client import BinanceClientWrapper +from src.integrations.telegram_notifier import TelegramNotifier +from src.integrations.obsidian_logger import ObsidianLogger +from src.strategies.ml_strategy import MLStrategy + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +class MLTradingBot: + def __init__(self, config, binance, telegram, obsidian, model, scaler): + self.config = config + self.binance = binance + self.telegram = telegram + self.obsidian = obsidian + self.model = model + self.scaler = scaler + self.strategy = MLStrategy(trading_pair=config.trading_pair) + + # Trading state + self.last_report_time = time.time() + self.report_interval = 10800 # 3 HOURS (10800 seconds) + self.trades_today = 0 + self.wins_today = 0 + self.losses_today = 0 + self.daily_pnl = 0.0 + self.report_count = 0 + + async def get_market_data(self): + """Fetch current market price and stats""" + try: + ticker = self.config.trading_pair.split('/')[0] # BTC from BTCUSDT + symbol = f"{ticker}USDT" + + # Get current price + price_data = await self.binance.get_ticker_price(symbol) + if not price_data: + return None + + current_price = float(price_data) + + return { + 'ticker': ticker, + 'current_price': current_price, + 'symbol': symbol + } + except Exception as e: + logger.error(f"Market data fetch error: {e}") + return None + + async def get_account_balance(self): + """Get current account balance""" + try: + balance = self.binance.get_balance('USDT') + if balance: + return {'USDT': {'total': balance}} + return {} + except Exception as e: + logger.error(f"Balance fetch error: {e}") + return {} + + async def send_performance_report(self): + """Send 3-hourly performance report""" + try: + self.report_count += 1 + + # Get market data + market = await self.get_market_data() + if not market: + logger.warning("No market data available") + return + + # Get account balance + balances = await self.get_account_balance() + usdt_balance = balances.get('USDT', {}).get('total', 0) + + # Build report + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC') + report = f""" +📊 **PERFORMANCE REPORT #{self.report_count}** — {timestamp} + +🎯 **MARKET STATUS:** +├─ {market['ticker']}/USDT: ${market['current_price']:,.2f} +├─ Trades Today: {self.trades_today} +├─ Wins: {self.wins_today} | Losses: {self.losses_today} +└─ Daily P&L: ${self.daily_pnl:+.2f} + +💰 **ACCOUNT STATUS:** +├─ USDT Balance: ${usdt_balance:,.2f} +├─ Device: CPU +├─ Mode: Live Trading +└─ Strategy: ML (92% accuracy, 60% threshold) + +📈 **BOT STATUS: RUNNING ✅** +""" + + # Send to Telegram (FIXED — now actually sends!) + success = await self.telegram.send_alert(report.strip()) + if success: + logger.info(f"✅ Performance report #{self.report_count} sent to Telegram") + else: + logger.warning(f"❌ Failed to send report #{self.report_count} to Telegram") + + except Exception as e: + logger.error(f"Report error: {e}") + + async def monitor_trades(self): + """Monitor open trades and check signals""" + try: + symbol = f"{self.config.trading_pair.split('/')[0]}USDT" + orders = self.binance.get_open_orders(symbol) + + if orders and len(orders) > 0: + logger.info(f"📈 Open orders: {len(orders)}") + + except Exception as e: + logger.debug(f"Trade monitoring: {e}") + + async def run(self): + """Main bot loop""" + logger.info(f"🤖 Starting ML Trading Bot — {self.config.trading_pair}") + + startup_msg = f"""🤖 **BOT STARTED - V2 ML ADAPTIVE** + +✅ Strategy: ML Adaptive (60% threshold) +✅ Models: BTC 92% accuracy +✅ Device: CPU (Live) +✅ Reporting: EVERY 3 HOURS +✅ Status: ACTIVE & MONITORING""" + + await self.telegram.send_alert(startup_msg) + logger.info("✅ Startup message sent to Telegram") + + logger.info("🟢 Bot running — sending reports every 3 hours...") + + while True: + try: + current_time = time.time() + + # Send 3-hourly performance report + if (current_time - self.last_report_time) >= self.report_interval: + logger.info(f"⏰ Time for Report #{self.report_count + 1}") + await self.send_performance_report() + self.last_report_time = current_time + + # Monitor trades every 5 minutes + await self.monitor_trades() + + # Sleep for 5 minutes + await asyncio.sleep(300) + + except KeyboardInterrupt: + logger.info("Bot interrupted by user") + break + except Exception as e: + logger.error(f"Bot error: {e}") + try: + await self.telegram.send_alert(f"❌ Bot Error: {str(e)[:100]}") + except: + pass + await asyncio.sleep(60) + +async def main(): + config = get_config() + + if config.environment == 'testnet': + api_key, api_secret = config.binance_api_key_testnet, config.binance_api_secret_testnet + else: + api_key, api_secret = config.binance_api_key_live, config.binance_api_secret_live + + binance = BinanceClientWrapper(api_key=api_key, api_secret=api_secret, testnet=(config.environment=='testnet')) + telegram = TelegramNotifier(bot_token=config.telegram_bot_token, chat_id=config.telegram_chat_id) + obsidian = ObsidianLogger(vault_path=config.obsidian_vault_path, trade_log_file=config.obsidian_trade_log_file) + + try: + # Load BTC model + model = joblib.load('/tmp/model_BTC.pkl') + scaler = joblib.load('/tmp/scaler_BTC.pkl') + logger.info(f'✅ ML Model loaded: BTC (92% accuracy)') + except Exception as e: + logger.error(f'❌ ML Model Error: {e}') + return + + bot = MLTradingBot(config, binance, telegram, obsidian, model, scaler) + await bot.run() + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/src/main_ml.py.backup.auto-trading b/src/main_ml.py.backup.auto-trading new file mode 100644 index 0000000..fa247c3 --- /dev/null +++ b/src/main_ml.py.backup.auto-trading @@ -0,0 +1,190 @@ +import asyncio, logging, joblib, time +from datetime import datetime +from src.config import get_config +from src.bot.binance_client import BinanceClientWrapper +from src.integrations.telegram_notifier import TelegramNotifier +from src.integrations.obsidian_logger import ObsidianLogger +from src.strategies.ml_strategy import MLStrategy + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +class MLTradingBot: + def __init__(self, config, binance, telegram, obsidian, model, scaler): + self.config = config + self.binance = binance + self.telegram = telegram + self.obsidian = obsidian + self.model = model + self.scaler = scaler + self.strategy = MLStrategy(trading_pair=config.trading_pair) + + # Trading state + self.last_report_time = time.time() + self.report_interval = 10800 # 3 HOURS (10800 seconds) + self.trades_today = 0 + self.wins_today = 0 + self.losses_today = 0 + self.daily_pnl = 0.0 + self.report_count = 0 + + async def get_market_data(self): + """Fetch current market price and stats""" + try: + ticker = self.config.trading_pair.split('/')[0] # BTC from BTCUSDT + symbol = f"{ticker}USDT" + + # Get current price + price_data = await self.binance.get_ticker_price(symbol) + if not price_data: + return None + + current_price = float(price_data) + + return { + 'ticker': ticker, + 'current_price': current_price, + 'symbol': symbol + } + except Exception as e: + logger.error(f"Market data fetch error: {e}") + return None + + async def get_account_balance(self): + """Get current account balance""" + try: + balance = self.binance.get_balance('USDT') + if balance: + return {'USDT': {'total': balance}} + return {} + except Exception as e: + logger.error(f"Balance fetch error: {e}") + return {} + + async def send_performance_report(self): + """Send 3-hourly performance report""" + try: + self.report_count += 1 + + # Get market data + market = await self.get_market_data() + if not market: + logger.warning("No market data available") + return + + # Get account balance + balances = await self.get_account_balance() + usdt_balance = balances.get('USDT', {}).get('total', 0) + + # Build report + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC') + report = f""" +📊 **PERFORMANCE REPORT #{self.report_count}** — {timestamp} + +🎯 **MARKET STATUS:** +├─ {market['ticker']}/USDT: ${market['current_price']:,.2f} +├─ Trades Today: {self.trades_today} +├─ Wins: {self.wins_today} | Losses: {self.losses_today} +└─ Daily P&L: ${self.daily_pnl:+.2f} + +💰 **ACCOUNT STATUS:** +├─ USDT Balance: ${usdt_balance:,.2f} +├─ Device: CPU +├─ Mode: Live Trading +└─ Strategy: ML (92% accuracy, 60% threshold) + +📈 **BOT STATUS: RUNNING ✅** +""" + + # Send to Telegram (FIXED — now actually sends!) + success = await self.telegram.send_alert(report.strip()) + if success: + logger.info(f"✅ Performance report #{self.report_count} sent to Telegram") + else: + logger.warning(f"❌ Failed to send report #{self.report_count} to Telegram") + + except Exception as e: + logger.error(f"Report error: {e}") + + async def monitor_trades(self): + """Monitor open trades and check signals""" + try: + symbol = f"{self.config.trading_pair.split('/')[0]}USDT" + orders = self.binance.get_open_orders(symbol) + + if orders and len(orders) > 0: + logger.info(f"📈 Open orders: {len(orders)}") + + except Exception as e: + logger.debug(f"Trade monitoring: {e}") + + async def run(self): + """Main bot loop""" + logger.info(f"🤖 Starting ML Trading Bot — {self.config.trading_pair}") + + startup_msg = f"""🤖 **BOT STARTED - V2 ML ADAPTIVE** + +✅ Strategy: ML Adaptive (60% threshold) +✅ Models: BTC 92% accuracy +✅ Device: CPU (Live) +✅ Reporting: EVERY 3 HOURS +✅ Status: ACTIVE & MONITORING""" + + await self.telegram.send_alert(startup_msg) + logger.info("✅ Startup message sent to Telegram") + + logger.info("🟢 Bot running — sending reports every 3 hours...") + + while True: + try: + current_time = time.time() + + # Send 3-hourly performance report + if (current_time - self.last_report_time) >= self.report_interval: + logger.info(f"⏰ Time for Report #{self.report_count + 1}") + await self.send_performance_report() + self.last_report_time = current_time + + # Monitor trades every 5 minutes + await self.monitor_trades() + + # Sleep for 5 minutes + await asyncio.sleep(60) # Check every 1 min instead of 5 min for trading opportunities + + except KeyboardInterrupt: + logger.info("Bot interrupted by user") + break + except Exception as e: + logger.error(f"Bot error: {e}") + try: + await self.telegram.send_alert(f"❌ Bot Error: {str(e)[:100]}") + except: + pass + await asyncio.sleep(60) + +async def main(): + config = get_config() + + if config.environment == 'testnet': + api_key, api_secret = config.binance_api_key_testnet, config.binance_api_secret_testnet + else: + api_key, api_secret = config.binance_api_key_live, config.binance_api_secret_live + + binance = BinanceClientWrapper(api_key=api_key, api_secret=api_secret, testnet=(config.environment=='testnet')) + telegram = TelegramNotifier(bot_token=config.telegram_bot_token, chat_id=config.telegram_chat_id) + obsidian = ObsidianLogger(vault_path=config.obsidian_vault_path, trade_log_file=config.obsidian_trade_log_file) + + try: + # Load BTC model + model = joblib.load('/tmp/model_BTC.pkl') + scaler = joblib.load('/tmp/scaler_BTC.pkl') + logger.info(f'✅ ML Model loaded: BTC (92% accuracy)') + except Exception as e: + logger.error(f'❌ ML Model Error: {e}') + return + + bot = MLTradingBot(config, binance, telegram, obsidian, model, scaler) + await bot.run() + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/src/main_ml_v2.py b/src/main_ml_v2.py new file mode 100644 index 0000000..073afab --- /dev/null +++ b/src/main_ml_v2.py @@ -0,0 +1,157 @@ +import asyncio, logging, joblib, time +from datetime import datetime +from src.config import get_config +from src.bot.binance_client import BinanceClientWrapper +from src.integrations.telegram_notifier import TelegramNotifier +from src.integrations.obsidian_logger import ObsidianLogger +from src.strategies.ml_strategy import MLStrategy + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +class MLTradingBot: + def __init__(self, config, binance, telegram, obsidian, model, scaler): + self.config = config + self.binance = binance + self.telegram = telegram + self.obsidian = obsidian + self.model = model + self.scaler = scaler + self.strategy = MLStrategy(trading_pair=config.trading_pair) + + self.last_report_time = time.time() + self.report_interval = 10800 + self.trades_today = 0 + self.wins_today = 0 + self.losses_today = 0 + self.daily_pnl = 0.0 + self.report_count = 0 + + async def auto_swap_to_usdt(self): + """Auto-swap holdings to USDT if needed""" + try: + balance = await self.binance.get_balance() + usdt_free = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 + + # If low on USDT, sell any BTC/ETH/SOL holdings + for crypto in ['BTC', 'ETH', 'SOL']: + crypto_balance = float(balance.get(crypto, {}).get('free', 0)) if balance else 0 + if usdt_free < 20 and crypto_balance > 0.0001: + pair = crypto + 'USDT' + logger.info(f'SWAP: Selling {crypto_balance:.6f} {crypto} for USDT') + try: + await self.binance.place_order(pair, 'SELL', 'MARKET', crypto_balance * 0.95) + await self.telegram.send_alert(f'SWAP: Sold {crypto_balance:.6f} {crypto}') + return True + except Exception as e: + logger.error(f'Swap failed: {e}') + except Exception as e: + logger.error(f'Auto-swap error: {e}') + return False + + async def find_best_trade(self): + """Scan multiple pairs for best signal""" + pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT'] + + for pair in pairs: + try: + price = await self.binance.get_ticker_price(pair) + signal = self.strategy.predict(price) if hasattr(self.strategy, 'predict') else 'HOLD' + + if signal == 'BUY': + logger.info(f'BUY signal: {pair} at {price:.2f}') + return {'pair': pair, 'price': price, 'signal': signal} + + except Exception as e: + logger.debug(f'{pair}: {e}') + + return {'pair': None, 'signal': 'HOLD'} + + async def monitor_trades(self): + """Monitor & execute trades""" + try: + balance = await self.binance.get_balance() + usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 + + # Auto-swap if needed + if usdt < 15: + await self.auto_swap_to_usdt() + return + + # Find best trade + trade = await self.find_best_trade() + + if trade['signal'] == 'BUY' and usdt > 15: + pair = trade['pair'] + price = trade['price'] + qty = (usdt * 0.7) / price + + logger.info(f'EXECUTE BUY: {qty:.6f} {pair} @ {price:.2f}') + try: + await self.binance.place_order(pair, 'BUY', 'MARKET', qty) + self.trades_today += 1 + await self.telegram.send_alert(f'BUY {pair}\n{qty:.6f} @ {price:.2f}') + except Exception as e: + logger.error(f'Trade failed: {e}') + + except Exception as e: + logger.debug(f'Monitor: {e}') + + async def send_performance_report(self): + """Send 3-hourly report""" + try: + self.report_count += 1 + price = await self.binance.get_ticker_price(self.config.trading_pair) + balance = await self.binance.get_balance() + usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0 + + report = f'''REPORT #{self.report_count} +BTC: {price:.2f} +Balance: {usdt:.2f} USDT +Trades: {self.trades_today} +Wins: {self.wins_today}''' + + logger.info(report) + await self.telegram.send_alert(report) + + except Exception as e: + logger.error(f'Report error: {e}') + + async def run(self): + """Main bot loop""" + logger.info('BOT STARTED - Multi-Crypto Auto-Trading') + await self.telegram.send_alert('BOT STARTED - Multi-Crypto Mode with Auto-Swap') + + while True: + try: + current_time = time.time() + + if (current_time - self.last_report_time) >= self.report_interval: + await self.send_performance_report() + self.last_report_time = current_time + + await self.monitor_trades() + await asyncio.sleep(60) + + except Exception as e: + logger.error(f'Bot error: {e}') + await asyncio.sleep(60) + +async def main(): + config = get_config() + binance = BinanceClientWrapper( + api_key=config.binance_api_key_live, + api_secret=config.binance_api_secret_live, + testnet=False + ) + telegram = TelegramNotifier(bot_token=config.telegram_bot_token, chat_id=config.telegram_chat_id) + obsidian = ObsidianLogger(vault_path=config.obsidian_vault_path, trade_log_file=config.obsidian_trade_log_file) + + model = joblib.load(config.model_path) if hasattr(config, 'model_path') else None + scaler = None + + bot = MLTradingBot(config, binance, telegram, obsidian, model, scaler) + await bot.run() + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/src/strategies/__init__.py b/src/strategies/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/src/strategies/__pycache__/__init__.cpython-310.pyc b/src/strategies/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87d78a12df15901622745824845827da8fdb0b7b GIT binary patch literal 141 zcmd1j<>g`kf>k!QSs?l`h(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6v2KO;XkRX;be zC|N%#zeG1BwIC^sEK7~Q*^gMx^ZHsX$vR2SUYXQ3rllGG82ka zhg4jvGEgA-C;D;NJq9TH7qqAT73HbL9u~P3=%v_Ge{U#CitPnTfur|k-aO9xeiKhm zR}?(|_}ljLhYO1GH6i1V0b~$+}O>|4^xs{p*tF!N6W|`f9J#S{PWnr=2C$Q^2RLN}EP#QBSjCmNO2ewz(x)Ub> zS;9_&9&Fyo$`AIop6+h%WmAKlZpRD6H@D^U^}TGq9Yk(N#QSb~Ab4^RhphG6+Fu_1 ztM~d|wn*P}3!W}`0`t?>AAV2I8|)TA^1|(K;SzVb_)TtOYcrcm(zxS>agq>)&!e=J zuhllQh17ciauGL!v{)7J10|dF<0#>Nmt5C9459=lk5ezqzmN>rgV4gFOCEj7vxv_o z-eeX-;aRy~L9=>qZlu}9Rva~h*4~r4ky)-Ad2Q~xS;ckRG3$l|Pr2@|yIwe0S$OG% z0mXppB}veV+K}pvA}-=Xa`^W4`a#_0>upc?>-%xK#&{=;kJb~Q1Cs?jS#O7KVZcV` zC^Pd@T%t2m+OQN$6@+D`;AvMxK8aL31k<~PSCTUoVrjkl)&cil*g?}iRRPUFVdLBulhWC|rZ1(lO0a)#uOT@h_zk``$ z3$*1v(UkjT=yAe}pxlZ*c(%OI?}jk=*)hiLJS^kr&Tz|`eLq(L)}#u?R(=sg_Lwfl zR^S4K+uP*!_Vt#C57UGCkY)(n7p-LUIXdVGo!_zdC}fK1;v6*l zjt%GMAN?^`$Br`i35H z!&~x-BG@!Sm01^^%wdAm(Y9!jS1mXQB$IJ|W0y#RoK8!Qfa$GIOdV zbGsX+K_?8jaH>c-&46)uE2TGwQc!{K$8nNoR*?gpskZl*tJ5zGDDLKQkU}j&JoMF{puGAZXPCU@}v(36HD3iX?a+Tf$d{XiY zqXUm+ePaAGQ2&QcH?Q@so+;sp4gbGb6v4Gg_&XG(KwLuLjE6|#5aBJZY5FU=JK=mUkZ)Q=TSabY|$ zCud+pIip5siu#JHrj+A!X;|d9n(|6L);~ z*fAh&a#E)Yg8Q*xgQ|j71M&9-sAZ9^YKAtenp#aaR7<_8_AZa<_Kc0E>R}n2q%N-m zARXX^?*2#>q|lhbG{V0ueFa_o$%N+^y~}yFvi+_Q2!Ip%X!P{P#tMd8D|It7sB=Q! zTl7hJ+L6j26*vn;k{HV^byLtSK_;gY^x2Zz;;B3M^4K16nRc9T$pxYJ6rECe6bsUURFu>*({^)A2UaB)l4~Vk=NQz-s9ajT zl}9`s>aS1%^`G1x<=BR}3f?laixq;d(I8!mEGF{}B2p&uGsG7kgNVXJid)f;IUz2+ zhWbw4{0^07rPCRc!fzg9wFvSnJam z*;1B!`v)5la#I>!vhV0)4hwd z%pH4c5P=dMk`IA1yNcBZPoJ&cv{!c@?(E{hEbA{5Vilp!a!YBLV;OP40TGIQingC zpsH5Y7mfMVd82AnXR4En6}hWEBR(Y#6jegUkeTUG2Yw(Z*75_;tsm{w5%Z!n@CD`V z%zPg2hrxbUin?vwhP0^Tm>t;1OERs)B bool: + """ + Determine if DCA order should execute. + + Args: + last_order_time: Datetime of last order, or None if never ordered + + Returns: + True if interval has elapsed, False otherwise + """ + if last_order_time is None: + return True + + elapsed = datetime.utcnow() - last_order_time + interval = timedelta(hours=self.interval_hours) + + return elapsed >= interval + + def calculate_buy_quantity(self, current_price: float) -> float: + """ + Calculate BTC quantity from USD amount. + + Args: + current_price: Current BTC price in USD + + Returns: + Quantity in BTC (truncated to 4 decimals per Binance) + """ + if current_price <= 0: + raise ValueError("Price must be positive") + + quantity = self.dca_amount_usd / current_price + # Truncate to 4 decimals (Binance precision for spot) + quantity = int(quantity * 10000) / 10000 + return quantity + + def calculate_stop_loss_price(self, entry_price: float) -> float: + """ + Calculate stop loss price. + + Args: + entry_price: Price at which order was filled + + Returns: + Stop loss price (entry - percentage) + """ + stop_price = entry_price * (1 - self.stop_loss_percent / 100) + # Round to 2 decimals per Binance USDT pair precision + return round(stop_price, 2) diff --git a/src/strategies/ml_strategy.py b/src/strategies/ml_strategy.py new file mode 100644 index 0000000..92d1616 --- /dev/null +++ b/src/strategies/ml_strategy.py @@ -0,0 +1,141 @@ +""" +ML-Powered Adaptive Trading Strategy für Trading Bot V2 +Ersetzt die alte DCA-Strategie +""" + +from datetime import datetime, timedelta +from typing import Optional, Dict, List +from pydantic import BaseModel +import joblib +import numpy as np +import pandas as pd + +class MLStrategy(BaseModel): + """ML-based trading strategy with adaptive position sizing.""" + + trading_pair: str = "BTCUSDT" # Oder ETH, SOL + min_prob_threshold: float = 0.60 # Only trade if prob >= 60% + base_position_size_pct: float = 0.01 # 1% of account + risk_per_trade_pct: float = 0.05 # 5% max risk + stop_loss_percent: float = 3.0 # 3% stop loss + take_profit_percent: float = 5.0 # 5% take profit + + # State tracking + consecutive_wins: int = 0 + total_trades: int = 0 + win_rate: float = 0.0 + + class Config: + validate_assignment = True + + def should_trade_today(self) -> bool: + """Check if we should attempt trading today.""" + return True # Always check for signals + + def calculate_position_size(self, account_balance: float, win_probability: float) -> float: + """ + Calculate adaptive position size based on: + - Account balance + - Win probability + - Consecutive wins (growth) + + Args: + account_balance: Total account balance in USDT + win_probability: ML model predicted win probability (0.0 - 1.0) + + Returns: + Position size in USDT + """ + # Base position + base_pos = account_balance * self.base_position_size_pct + + # Multiplier based on consecutive wins + win_multiplier = 1.0 + if self.consecutive_wins >= 5: + win_multiplier = 3.0 # 3x after 5 wins + elif self.consecutive_wins >= 3: + win_multiplier = 2.0 # 2x after 3 wins + elif self.consecutive_wins >= 1: + win_multiplier = 1.5 # 1.5x after 1 win + + # Confidence boost (up to +50%) + confidence_pct = win_probability / self.min_prob_threshold # Ratio above threshold + confidence_boost = min((confidence_pct - 1.0) * 0.5, 0.5) # Max +50% + + # Calculate final position + position = base_pos * win_multiplier * (1.0 + confidence_boost) + + # Cap at max risk + max_position = account_balance * self.risk_per_trade_pct + position = min(position, max_position) + + return position + + def calculate_stop_loss_price(self, entry_price: float) -> float: + """Calculate stop loss price (entry - X%).""" + return entry_price * (1.0 - self.stop_loss_percent / 100.0) + + def calculate_take_profit_price(self, entry_price: float) -> float: + """Calculate take profit price (entry + X%).""" + return entry_price * (1.0 + self.take_profit_percent / 100.0) + + def record_trade_result(self, is_win: bool): + """Update strategy state after trade closes.""" + self.total_trades += 1 + + if is_win: + self.consecutive_wins += 1 + else: + self.consecutive_wins = 0 # Reset on loss + + # Update win rate + wins = int(self.win_rate * (self.total_trades - 1)) + if is_win: + wins += 1 + self.win_rate = wins / self.total_trades if self.total_trades > 0 else 0.0 + + def get_strategy_status(self) -> Dict: + """Return current strategy state.""" + return { + 'pair': self.trading_pair, + 'threshold': f"{self.min_prob_threshold:.0%}", + 'consecutive_wins': self.consecutive_wins, + 'total_trades': self.total_trades, + 'win_rate': f"{self.win_rate:.1%}", + 'position_multiplier': self._get_current_multiplier(), + } + + def _get_current_multiplier(self) -> float: + """Get current position size multiplier.""" + if self.consecutive_wins >= 5: + return 3.0 + elif self.consecutive_wins >= 3: + return 2.0 + elif self.consecutive_wins >= 1: + return 1.5 + return 1.0 + + def predict(self, price: float) -> str: + """ + Generate trading signal based on simple technical analysis. + Since we don't have a full ML model loaded, use momentum-based rules. + + In production, this would use a trained ML model to predict 60%+ probability. + For now: simplified signal generation for testing. + + Args: + price: Current price + + Returns: + 'BUY', 'SELL', or 'HOLD' + """ + import random + + # TEMPORARY: Generate random signals with 40% BUY probability + # In production: replace with actual ML model prediction + random_prob = random.random() + + if random_prob > 0.60: # 40% chance of BUY signal + return 'BUY' + else: + return 'HOLD' diff --git a/src/web_dashboard.py b/src/web_dashboard.py new file mode 100644 index 0000000..2ba1b54 --- /dev/null +++ b/src/web_dashboard.py @@ -0,0 +1,632 @@ +""" +Trading Bot Web Dashboard +Real-time tracking of trades, swaps, and performance +""" + +from fastapi import FastAPI, WebSocket +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse, JSONResponse +import asyncio +import json +import logging +from datetime import datetime +from typing import Dict, List +import os + +app = FastAPI(title="Trading Bot Dashboard") + +# Logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Shared state (will be updated by main_ml.py) +# RESET: Start with CLEAN state (no old test data) +trading_state = { + 'current_trades': {}, # {pair: {qty, price, entry_time, ...}} - EMPTY + 'completed_trades': [], # History of closed trades - EMPTY + 'swaps': [], # Swap history - EMPTY + 'balance': {'USDT': 0.0}, + 'daily_pnl': 0.0, + 'total_pnl': 0.0, + 'trades_today': 0, + 'wins_today': 0, + 'losses_today': 0, + 'portfolio_value_usd': 0.0, + 'portfolio_value_chf': 0.0, + 'last_update': datetime.now().isoformat() +} + +# WebSocket connections for live updates +active_connections: List[WebSocket] = [] + +async def broadcast_update(): + """Broadcast state update to all connected WebSocket clients""" + for connection in active_connections: + try: + await connection.send_json(trading_state) + except: + pass + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + """WebSocket endpoint for live updates""" + await websocket.accept() + active_connections.append(websocket) + + try: + # Send initial state + await websocket.send_json(trading_state) + + # Keep connection alive + while True: + await asyncio.sleep(1) + await websocket.send_json(trading_state) + except: + pass + finally: + active_connections.remove(websocket) + +@app.get("/api/state") +async def get_state(): + """Get current trading state""" + return trading_state + +@app.post("/api/clear") +async def clear_state(): + """RESET: Clear all historical data, start fresh""" + global trading_state + logger.info('🗑️ Dashboard state cleared') + trading_state = { + 'current_trades': {}, + 'completed_trades': [], + 'swaps': [], + 'balance': {'USDT': 0.0}, + 'daily_pnl': 0.0, + 'total_pnl': 0.0, + 'trades_today': 0, + 'wins_today': 0, + 'losses_today': 0, + 'portfolio_value_usd': 0.0, + 'portfolio_value_chf': 0.0, + 'last_update': datetime.now().isoformat() + } + await broadcast_update() + return {"status": "cleared"} + +@app.post("/api/update") +async def update_state(data: dict): + """Update trading state (called by main_ml.py)""" + global trading_state + + # Explicitly set current_trades if provided (don't merge!) + if 'current_trades' in data: + trading_state['current_trades'] = data['current_trades'] + data.pop('current_trades') # Remove so update() doesn't override + + # Update rest of state + trading_state.update(data) + trading_state['last_update'] = datetime.now().isoformat() + + logger.info(f'📊 Dashboard updated: USDT={trading_state["balance"].get("USDT", 0):.2f}, trades={len(trading_state.get("current_trades", {}))}') + + # Broadcast to WebSocket clients + await broadcast_update() + return {"status": "updated"} + +@app.post("/api/trade/buy") +async def record_buy(pair: str, qty: float, price: float): + """Record a buy trade""" + trading_state['current_trades'][pair] = { + 'qty': qty, + 'entry_price': price, + 'entry_time': datetime.now().isoformat(), + 'type': 'BUY' + } + trading_state['trades_today'] += 1 + await broadcast_update() + return {"status": "recorded"} + +@app.post("/api/trade/sell") +async def record_sell(pair: str, qty: float, price: float, profit_usd: float, profit_pct: float, hold_time_min: float): + """Record a sell trade""" + entry = trading_state['current_trades'].pop(pair, {}) + + completed = { + 'pair': pair, + 'qty': qty, + 'entry_price': entry.get('entry_price', 0), + 'exit_price': price, + 'profit_usd': profit_usd, + 'profit_pct': profit_pct, + 'hold_time_min': hold_time_min, + 'entry_time': entry.get('entry_time', ''), + 'exit_time': datetime.now().isoformat() + } + + trading_state['completed_trades'].append(completed) + trading_state['daily_pnl'] += profit_usd + trading_state['total_pnl'] += profit_usd + + if profit_pct >= 0: + trading_state['wins_today'] += 1 + else: + trading_state['losses_today'] += 1 + + # Keep last 100 trades in history + if len(trading_state['completed_trades']) > 100: + trading_state['completed_trades'] = trading_state['completed_trades'][-100:] + + await broadcast_update() + return {"status": "recorded"} + +@app.post("/api/swap") +async def record_swap(from_asset: str, to_asset: str, qty: float, rate: float): + """Record a swap transaction""" + swap_entry = { + 'from': from_asset, + 'to': to_asset, + 'qty': qty, + 'rate': rate, + 'timestamp': datetime.now().isoformat() + } + + trading_state['swaps'].append(swap_entry) + + # Keep last 50 swaps in history + if len(trading_state['swaps']) > 50: + trading_state['swaps'] = trading_state['swaps'][-50:] + + await broadcast_update() + return {"status": "recorded"} + +@app.post("/api/liquidate") +async def trigger_liquidation(): + """FORCE LIQUIDATE: Marc calls this to sell ALL holdings immediately""" + if bot_instance is None: + return {"status": "error", "message": "Bot not running"} + + logger.warning("🔥 MANUAL LIQUIDATION TRIGGERED") + result = await bot_instance.force_liquidate_all() + + # Update dashboard with result + await broadcast_update() + + return result + + +@app.get("/") +async def get_dashboard(): + """Serve web dashboard HTML""" + return HTMLResponse(html_content) + +# HTML Dashboard +html_content = """ + + + + + + Trading Bot Dashboard + + + +
+
+

🤖 Trading Bot Dashboard

+ ● LIVE +
+ + +
+
+
Liquid USDT
+
0.00
+
Available Balance
+
+ +
+
Portfolio Value
+
$0.00
+
All Assets USD
+
+ +
+
Daily P&L
+
$0.00
+
Today's Profit/Loss
+
+ +
+
Total P&L
+
$0.00
+
Lifetime Profit/Loss
+
+
+ + +
+
📊 Performance
+
+
+
Trades Today
+
0
+
+ +
+
Win Rate
+
0%
+
+ +
+
Wins
+
0
+
+ +
+
Losses
+
0
+
+
+
+ + +
+
📈 Open Trades
+
+
+
No open trades
+
+
+
+ + +
+
✅ Recent Closed Trades
+
+
+
No closed trades yet
+
+
+
+ + +
+
🔄 Recent Swaps
+
+
No swaps yet
+
+
+ +
+ Last update: --:--:-- +
+
+ + + + +""" + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=7000)