crca 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (501) hide show
  1. .github/ISSUE_TEMPLATE/bug_report.md +65 -0
  2. .github/ISSUE_TEMPLATE/feature_request.md +41 -0
  3. .github/PULL_REQUEST_TEMPLATE.md +20 -0
  4. .github/workflows/publish-manual.yml +61 -0
  5. .github/workflows/publish.yml +64 -0
  6. .gitignore +214 -0
  7. CRCA.py +4156 -0
  8. LICENSE +201 -0
  9. MANIFEST.in +43 -0
  10. PKG-INFO +5035 -0
  11. README.md +4959 -0
  12. __init__.py +17 -0
  13. branches/CRCA-Q.py +2728 -0
  14. branches/crca_cg/corposwarm.py +9065 -0
  15. branches/crca_cg/fix_rancher_docker_creds.ps1 +155 -0
  16. branches/crca_cg/package.json +5 -0
  17. branches/crca_cg/test_bolt_integration.py +446 -0
  18. branches/crca_cg/test_corposwarm_comprehensive.py +773 -0
  19. branches/crca_cg/test_new_features.py +163 -0
  20. branches/crca_sd/__init__.py +149 -0
  21. branches/crca_sd/crca_sd_core.py +770 -0
  22. branches/crca_sd/crca_sd_governance.py +1325 -0
  23. branches/crca_sd/crca_sd_mpc.py +1130 -0
  24. branches/crca_sd/crca_sd_realtime.py +1844 -0
  25. branches/crca_sd/crca_sd_tui.py +1133 -0
  26. crca-1.4.0.dist-info/METADATA +5035 -0
  27. crca-1.4.0.dist-info/RECORD +501 -0
  28. crca-1.4.0.dist-info/WHEEL +4 -0
  29. crca-1.4.0.dist-info/licenses/LICENSE +201 -0
  30. docs/CRCA-Q.md +2333 -0
  31. examples/config.yaml.example +25 -0
  32. examples/crca_sd_example.py +513 -0
  33. examples/data_broker_example.py +294 -0
  34. examples/logistics_corporation.py +861 -0
  35. examples/palantir_example.py +299 -0
  36. examples/policy_bench.py +934 -0
  37. examples/pridnestrovia-sd.py +705 -0
  38. examples/pridnestrovia_realtime.py +1902 -0
  39. prompts/__init__.py +10 -0
  40. prompts/default_crca.py +101 -0
  41. pyproject.toml +151 -0
  42. requirements.txt +76 -0
  43. schemas/__init__.py +43 -0
  44. schemas/mcpSchemas.py +51 -0
  45. schemas/policy.py +458 -0
  46. templates/__init__.py +38 -0
  47. templates/base_specialized_agent.py +195 -0
  48. templates/drift_detection.py +325 -0
  49. templates/examples/causal_agent_template.py +309 -0
  50. templates/examples/drag_drop_example.py +213 -0
  51. templates/examples/logistics_agent_template.py +207 -0
  52. templates/examples/trading_agent_template.py +206 -0
  53. templates/feature_mixins.py +253 -0
  54. templates/graph_management.py +442 -0
  55. templates/llm_integration.py +194 -0
  56. templates/module_registry.py +276 -0
  57. templates/mpc_planner.py +280 -0
  58. templates/policy_loop.py +1168 -0
  59. templates/prediction_framework.py +448 -0
  60. templates/statistical_methods.py +778 -0
  61. tests/sanity.yml +31 -0
  62. tests/sanity_check +406 -0
  63. tests/test_core.py +47 -0
  64. tests/test_crca_excel.py +166 -0
  65. tests/test_crca_sd.py +780 -0
  66. tests/test_data_broker.py +424 -0
  67. tests/test_palantir.py +349 -0
  68. tools/__init__.py +38 -0
  69. tools/actuators.py +437 -0
  70. tools/bolt.diy/Dockerfile +103 -0
  71. tools/bolt.diy/app/components/@settings/core/AvatarDropdown.tsx +175 -0
  72. tools/bolt.diy/app/components/@settings/core/ControlPanel.tsx +345 -0
  73. tools/bolt.diy/app/components/@settings/core/constants.tsx +108 -0
  74. tools/bolt.diy/app/components/@settings/core/types.ts +114 -0
  75. tools/bolt.diy/app/components/@settings/index.ts +12 -0
  76. tools/bolt.diy/app/components/@settings/shared/components/TabTile.tsx +151 -0
  77. tools/bolt.diy/app/components/@settings/shared/service-integration/ConnectionForm.tsx +193 -0
  78. tools/bolt.diy/app/components/@settings/shared/service-integration/ConnectionTestIndicator.tsx +60 -0
  79. tools/bolt.diy/app/components/@settings/shared/service-integration/ErrorState.tsx +102 -0
  80. tools/bolt.diy/app/components/@settings/shared/service-integration/LoadingState.tsx +94 -0
  81. tools/bolt.diy/app/components/@settings/shared/service-integration/ServiceHeader.tsx +72 -0
  82. tools/bolt.diy/app/components/@settings/shared/service-integration/index.ts +6 -0
  83. tools/bolt.diy/app/components/@settings/tabs/data/DataTab.tsx +721 -0
  84. tools/bolt.diy/app/components/@settings/tabs/data/DataVisualization.tsx +384 -0
  85. tools/bolt.diy/app/components/@settings/tabs/event-logs/EventLogsTab.tsx +1013 -0
  86. tools/bolt.diy/app/components/@settings/tabs/features/FeaturesTab.tsx +295 -0
  87. tools/bolt.diy/app/components/@settings/tabs/github/GitHubTab.tsx +281 -0
  88. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubAuthDialog.tsx +173 -0
  89. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubCacheManager.tsx +367 -0
  90. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubConnection.tsx +233 -0
  91. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubErrorBoundary.tsx +105 -0
  92. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubProgressiveLoader.tsx +266 -0
  93. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubRepositoryCard.tsx +121 -0
  94. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubRepositorySelector.tsx +312 -0
  95. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubStats.tsx +291 -0
  96. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubUserProfile.tsx +46 -0
  97. tools/bolt.diy/app/components/@settings/tabs/github/components/shared/GitHubStateIndicators.tsx +264 -0
  98. tools/bolt.diy/app/components/@settings/tabs/github/components/shared/RepositoryCard.tsx +361 -0
  99. tools/bolt.diy/app/components/@settings/tabs/github/components/shared/index.ts +11 -0
  100. tools/bolt.diy/app/components/@settings/tabs/gitlab/GitLabTab.tsx +305 -0
  101. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabAuthDialog.tsx +186 -0
  102. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabConnection.tsx +253 -0
  103. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabRepositorySelector.tsx +358 -0
  104. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/RepositoryCard.tsx +79 -0
  105. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/RepositoryList.tsx +142 -0
  106. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/StatsDisplay.tsx +91 -0
  107. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/index.ts +4 -0
  108. tools/bolt.diy/app/components/@settings/tabs/mcp/McpServerList.tsx +99 -0
  109. tools/bolt.diy/app/components/@settings/tabs/mcp/McpServerListItem.tsx +70 -0
  110. tools/bolt.diy/app/components/@settings/tabs/mcp/McpStatusBadge.tsx +37 -0
  111. tools/bolt.diy/app/components/@settings/tabs/mcp/McpTab.tsx +239 -0
  112. tools/bolt.diy/app/components/@settings/tabs/netlify/NetlifyTab.tsx +1393 -0
  113. tools/bolt.diy/app/components/@settings/tabs/netlify/components/NetlifyConnection.tsx +990 -0
  114. tools/bolt.diy/app/components/@settings/tabs/netlify/components/index.ts +1 -0
  115. tools/bolt.diy/app/components/@settings/tabs/notifications/NotificationsTab.tsx +300 -0
  116. tools/bolt.diy/app/components/@settings/tabs/profile/ProfileTab.tsx +181 -0
  117. tools/bolt.diy/app/components/@settings/tabs/providers/cloud/CloudProvidersTab.tsx +308 -0
  118. tools/bolt.diy/app/components/@settings/tabs/providers/local/ErrorBoundary.tsx +68 -0
  119. tools/bolt.diy/app/components/@settings/tabs/providers/local/HealthStatusBadge.tsx +64 -0
  120. tools/bolt.diy/app/components/@settings/tabs/providers/local/LoadingSkeleton.tsx +107 -0
  121. tools/bolt.diy/app/components/@settings/tabs/providers/local/LocalProvidersTab.tsx +556 -0
  122. tools/bolt.diy/app/components/@settings/tabs/providers/local/ModelCard.tsx +106 -0
  123. tools/bolt.diy/app/components/@settings/tabs/providers/local/ProviderCard.tsx +120 -0
  124. tools/bolt.diy/app/components/@settings/tabs/providers/local/SetupGuide.tsx +671 -0
  125. tools/bolt.diy/app/components/@settings/tabs/providers/local/StatusDashboard.tsx +91 -0
  126. tools/bolt.diy/app/components/@settings/tabs/providers/local/types.ts +44 -0
  127. tools/bolt.diy/app/components/@settings/tabs/settings/SettingsTab.tsx +215 -0
  128. tools/bolt.diy/app/components/@settings/tabs/supabase/SupabaseTab.tsx +1089 -0
  129. tools/bolt.diy/app/components/@settings/tabs/vercel/VercelTab.tsx +909 -0
  130. tools/bolt.diy/app/components/@settings/tabs/vercel/components/VercelConnection.tsx +368 -0
  131. tools/bolt.diy/app/components/@settings/tabs/vercel/components/index.ts +1 -0
  132. tools/bolt.diy/app/components/@settings/utils/tab-helpers.ts +54 -0
  133. tools/bolt.diy/app/components/chat/APIKeyManager.tsx +169 -0
  134. tools/bolt.diy/app/components/chat/Artifact.tsx +296 -0
  135. tools/bolt.diy/app/components/chat/AssistantMessage.tsx +192 -0
  136. tools/bolt.diy/app/components/chat/BaseChat.module.scss +47 -0
  137. tools/bolt.diy/app/components/chat/BaseChat.tsx +522 -0
  138. tools/bolt.diy/app/components/chat/Chat.client.tsx +670 -0
  139. tools/bolt.diy/app/components/chat/ChatAlert.tsx +108 -0
  140. tools/bolt.diy/app/components/chat/ChatBox.tsx +334 -0
  141. tools/bolt.diy/app/components/chat/CodeBlock.module.scss +10 -0
  142. tools/bolt.diy/app/components/chat/CodeBlock.tsx +85 -0
  143. tools/bolt.diy/app/components/chat/DicussMode.tsx +17 -0
  144. tools/bolt.diy/app/components/chat/ExamplePrompts.tsx +37 -0
  145. tools/bolt.diy/app/components/chat/FilePreview.tsx +38 -0
  146. tools/bolt.diy/app/components/chat/GitCloneButton.tsx +327 -0
  147. tools/bolt.diy/app/components/chat/ImportFolderButton.tsx +141 -0
  148. tools/bolt.diy/app/components/chat/LLMApiAlert.tsx +109 -0
  149. tools/bolt.diy/app/components/chat/MCPTools.tsx +129 -0
  150. tools/bolt.diy/app/components/chat/Markdown.module.scss +171 -0
  151. tools/bolt.diy/app/components/chat/Markdown.spec.ts +48 -0
  152. tools/bolt.diy/app/components/chat/Markdown.tsx +252 -0
  153. tools/bolt.diy/app/components/chat/Messages.client.tsx +102 -0
  154. tools/bolt.diy/app/components/chat/ModelSelector.tsx +797 -0
  155. tools/bolt.diy/app/components/chat/NetlifyDeploymentLink.client.tsx +51 -0
  156. tools/bolt.diy/app/components/chat/ProgressCompilation.tsx +110 -0
  157. tools/bolt.diy/app/components/chat/ScreenshotStateManager.tsx +33 -0
  158. tools/bolt.diy/app/components/chat/SendButton.client.tsx +39 -0
  159. tools/bolt.diy/app/components/chat/SpeechRecognition.tsx +28 -0
  160. tools/bolt.diy/app/components/chat/StarterTemplates.tsx +38 -0
  161. tools/bolt.diy/app/components/chat/SupabaseAlert.tsx +199 -0
  162. tools/bolt.diy/app/components/chat/SupabaseConnection.tsx +339 -0
  163. tools/bolt.diy/app/components/chat/ThoughtBox.tsx +43 -0
  164. tools/bolt.diy/app/components/chat/ToolInvocations.tsx +409 -0
  165. tools/bolt.diy/app/components/chat/UserMessage.tsx +101 -0
  166. tools/bolt.diy/app/components/chat/VercelDeploymentLink.client.tsx +158 -0
  167. tools/bolt.diy/app/components/chat/chatExportAndImport/ExportChatButton.tsx +49 -0
  168. tools/bolt.diy/app/components/chat/chatExportAndImport/ImportButtons.tsx +96 -0
  169. tools/bolt.diy/app/components/deploy/DeployAlert.tsx +197 -0
  170. tools/bolt.diy/app/components/deploy/DeployButton.tsx +277 -0
  171. tools/bolt.diy/app/components/deploy/GitHubDeploy.client.tsx +171 -0
  172. tools/bolt.diy/app/components/deploy/GitHubDeploymentDialog.tsx +1041 -0
  173. tools/bolt.diy/app/components/deploy/GitLabDeploy.client.tsx +171 -0
  174. tools/bolt.diy/app/components/deploy/GitLabDeploymentDialog.tsx +764 -0
  175. tools/bolt.diy/app/components/deploy/NetlifyDeploy.client.tsx +246 -0
  176. tools/bolt.diy/app/components/deploy/VercelDeploy.client.tsx +235 -0
  177. tools/bolt.diy/app/components/editor/codemirror/BinaryContent.tsx +7 -0
  178. tools/bolt.diy/app/components/editor/codemirror/CodeMirrorEditor.tsx +555 -0
  179. tools/bolt.diy/app/components/editor/codemirror/EnvMasking.ts +80 -0
  180. tools/bolt.diy/app/components/editor/codemirror/cm-theme.ts +192 -0
  181. tools/bolt.diy/app/components/editor/codemirror/indent.ts +68 -0
  182. tools/bolt.diy/app/components/editor/codemirror/languages.ts +112 -0
  183. tools/bolt.diy/app/components/git/GitUrlImport.client.tsx +147 -0
  184. tools/bolt.diy/app/components/header/Header.tsx +42 -0
  185. tools/bolt.diy/app/components/header/HeaderActionButtons.client.tsx +54 -0
  186. tools/bolt.diy/app/components/mandate/MandateSubmission.tsx +167 -0
  187. tools/bolt.diy/app/components/observability/DeploymentStatus.tsx +168 -0
  188. tools/bolt.diy/app/components/observability/EventTimeline.tsx +119 -0
  189. tools/bolt.diy/app/components/observability/FileDiffViewer.tsx +121 -0
  190. tools/bolt.diy/app/components/observability/GovernanceStatus.tsx +197 -0
  191. tools/bolt.diy/app/components/observability/GovernorMetrics.tsx +246 -0
  192. tools/bolt.diy/app/components/observability/LogStream.tsx +244 -0
  193. tools/bolt.diy/app/components/observability/MandateDetails.tsx +201 -0
  194. tools/bolt.diy/app/components/observability/ObservabilityDashboard.tsx +200 -0
  195. tools/bolt.diy/app/components/sidebar/HistoryItem.tsx +187 -0
  196. tools/bolt.diy/app/components/sidebar/Menu.client.tsx +536 -0
  197. tools/bolt.diy/app/components/sidebar/date-binning.ts +59 -0
  198. tools/bolt.diy/app/components/txt +1 -0
  199. tools/bolt.diy/app/components/ui/BackgroundRays/index.tsx +18 -0
  200. tools/bolt.diy/app/components/ui/BackgroundRays/styles.module.scss +246 -0
  201. tools/bolt.diy/app/components/ui/Badge.tsx +53 -0
  202. tools/bolt.diy/app/components/ui/BranchSelector.tsx +270 -0
  203. tools/bolt.diy/app/components/ui/Breadcrumbs.tsx +101 -0
  204. tools/bolt.diy/app/components/ui/Button.tsx +46 -0
  205. tools/bolt.diy/app/components/ui/Card.tsx +55 -0
  206. tools/bolt.diy/app/components/ui/Checkbox.tsx +32 -0
  207. tools/bolt.diy/app/components/ui/CloseButton.tsx +49 -0
  208. tools/bolt.diy/app/components/ui/CodeBlock.tsx +103 -0
  209. tools/bolt.diy/app/components/ui/Collapsible.tsx +9 -0
  210. tools/bolt.diy/app/components/ui/ColorSchemeDialog.tsx +378 -0
  211. tools/bolt.diy/app/components/ui/Dialog.tsx +449 -0
  212. tools/bolt.diy/app/components/ui/Dropdown.tsx +63 -0
  213. tools/bolt.diy/app/components/ui/EmptyState.tsx +154 -0
  214. tools/bolt.diy/app/components/ui/FileIcon.tsx +346 -0
  215. tools/bolt.diy/app/components/ui/FilterChip.tsx +92 -0
  216. tools/bolt.diy/app/components/ui/GlowingEffect.tsx +192 -0
  217. tools/bolt.diy/app/components/ui/GradientCard.tsx +100 -0
  218. tools/bolt.diy/app/components/ui/IconButton.tsx +84 -0
  219. tools/bolt.diy/app/components/ui/Input.tsx +22 -0
  220. tools/bolt.diy/app/components/ui/Label.tsx +20 -0
  221. tools/bolt.diy/app/components/ui/LoadingDots.tsx +27 -0
  222. tools/bolt.diy/app/components/ui/LoadingOverlay.tsx +32 -0
  223. tools/bolt.diy/app/components/ui/PanelHeader.tsx +20 -0
  224. tools/bolt.diy/app/components/ui/PanelHeaderButton.tsx +36 -0
  225. tools/bolt.diy/app/components/ui/Popover.tsx +29 -0
  226. tools/bolt.diy/app/components/ui/Progress.tsx +22 -0
  227. tools/bolt.diy/app/components/ui/RepositoryStats.tsx +87 -0
  228. tools/bolt.diy/app/components/ui/ScrollArea.tsx +41 -0
  229. tools/bolt.diy/app/components/ui/SearchInput.tsx +80 -0
  230. tools/bolt.diy/app/components/ui/SearchResultItem.tsx +134 -0
  231. tools/bolt.diy/app/components/ui/Separator.tsx +22 -0
  232. tools/bolt.diy/app/components/ui/SettingsButton.tsx +35 -0
  233. tools/bolt.diy/app/components/ui/Slider.tsx +73 -0
  234. tools/bolt.diy/app/components/ui/StatusIndicator.tsx +90 -0
  235. tools/bolt.diy/app/components/ui/Switch.tsx +37 -0
  236. tools/bolt.diy/app/components/ui/Tabs.tsx +52 -0
  237. tools/bolt.diy/app/components/ui/TabsWithSlider.tsx +112 -0
  238. tools/bolt.diy/app/components/ui/ThemeSwitch.tsx +29 -0
  239. tools/bolt.diy/app/components/ui/Tooltip.tsx +122 -0
  240. tools/bolt.diy/app/components/ui/index.ts +38 -0
  241. tools/bolt.diy/app/components/ui/use-toast.ts +66 -0
  242. tools/bolt.diy/app/components/workbench/DiffView.tsx +796 -0
  243. tools/bolt.diy/app/components/workbench/EditorPanel.tsx +174 -0
  244. tools/bolt.diy/app/components/workbench/ExpoQrModal.tsx +55 -0
  245. tools/bolt.diy/app/components/workbench/FileBreadcrumb.tsx +150 -0
  246. tools/bolt.diy/app/components/workbench/FileTree.tsx +565 -0
  247. tools/bolt.diy/app/components/workbench/Inspector.tsx +126 -0
  248. tools/bolt.diy/app/components/workbench/InspectorPanel.tsx +146 -0
  249. tools/bolt.diy/app/components/workbench/LockManager.tsx +262 -0
  250. tools/bolt.diy/app/components/workbench/PortDropdown.tsx +91 -0
  251. tools/bolt.diy/app/components/workbench/Preview.tsx +1049 -0
  252. tools/bolt.diy/app/components/workbench/ScreenshotSelector.tsx +293 -0
  253. tools/bolt.diy/app/components/workbench/Search.tsx +257 -0
  254. tools/bolt.diy/app/components/workbench/Workbench.client.tsx +506 -0
  255. tools/bolt.diy/app/components/workbench/terminal/Terminal.tsx +131 -0
  256. tools/bolt.diy/app/components/workbench/terminal/TerminalManager.tsx +68 -0
  257. tools/bolt.diy/app/components/workbench/terminal/TerminalTabs.tsx +277 -0
  258. tools/bolt.diy/app/components/workbench/terminal/theme.ts +36 -0
  259. tools/bolt.diy/app/components/workflow/WorkflowPhase.tsx +109 -0
  260. tools/bolt.diy/app/components/workflow/WorkflowStatus.tsx +60 -0
  261. tools/bolt.diy/app/components/workflow/WorkflowTimeline.tsx +150 -0
  262. tools/bolt.diy/app/entry.client.tsx +7 -0
  263. tools/bolt.diy/app/entry.server.tsx +80 -0
  264. tools/bolt.diy/app/root.tsx +156 -0
  265. tools/bolt.diy/app/routes/_index.tsx +175 -0
  266. tools/bolt.diy/app/routes/api.bug-report.ts +254 -0
  267. tools/bolt.diy/app/routes/api.chat.ts +463 -0
  268. tools/bolt.diy/app/routes/api.check-env-key.ts +41 -0
  269. tools/bolt.diy/app/routes/api.configured-providers.ts +110 -0
  270. tools/bolt.diy/app/routes/api.corporate-swarm-status.ts +55 -0
  271. tools/bolt.diy/app/routes/api.enhancer.ts +137 -0
  272. tools/bolt.diy/app/routes/api.export-api-keys.ts +44 -0
  273. tools/bolt.diy/app/routes/api.git-info.ts +69 -0
  274. tools/bolt.diy/app/routes/api.git-proxy.$.ts +178 -0
  275. tools/bolt.diy/app/routes/api.github-branches.ts +166 -0
  276. tools/bolt.diy/app/routes/api.github-deploy.ts +67 -0
  277. tools/bolt.diy/app/routes/api.github-stats.ts +198 -0
  278. tools/bolt.diy/app/routes/api.github-template.ts +242 -0
  279. tools/bolt.diy/app/routes/api.github-user.ts +287 -0
  280. tools/bolt.diy/app/routes/api.gitlab-branches.ts +143 -0
  281. tools/bolt.diy/app/routes/api.gitlab-deploy.ts +67 -0
  282. tools/bolt.diy/app/routes/api.gitlab-projects.ts +105 -0
  283. tools/bolt.diy/app/routes/api.health.ts +8 -0
  284. tools/bolt.diy/app/routes/api.llmcall.ts +298 -0
  285. tools/bolt.diy/app/routes/api.mandate.ts +351 -0
  286. tools/bolt.diy/app/routes/api.mcp-check.ts +16 -0
  287. tools/bolt.diy/app/routes/api.mcp-update-config.ts +23 -0
  288. tools/bolt.diy/app/routes/api.models.$provider.ts +2 -0
  289. tools/bolt.diy/app/routes/api.models.ts +90 -0
  290. tools/bolt.diy/app/routes/api.netlify-deploy.ts +240 -0
  291. tools/bolt.diy/app/routes/api.netlify-user.ts +142 -0
  292. tools/bolt.diy/app/routes/api.supabase-user.ts +199 -0
  293. tools/bolt.diy/app/routes/api.supabase.query.ts +92 -0
  294. tools/bolt.diy/app/routes/api.supabase.ts +56 -0
  295. tools/bolt.diy/app/routes/api.supabase.variables.ts +32 -0
  296. tools/bolt.diy/app/routes/api.system.diagnostics.ts +142 -0
  297. tools/bolt.diy/app/routes/api.system.disk-info.ts +311 -0
  298. tools/bolt.diy/app/routes/api.system.git-info.ts +332 -0
  299. tools/bolt.diy/app/routes/api.update.ts +21 -0
  300. tools/bolt.diy/app/routes/api.vercel-deploy.ts +497 -0
  301. tools/bolt.diy/app/routes/api.vercel-user.ts +161 -0
  302. tools/bolt.diy/app/routes/api.workflow-status.$proposalId.ts +309 -0
  303. tools/bolt.diy/app/routes/chat.$id.tsx +8 -0
  304. tools/bolt.diy/app/routes/execute.$mandateId.tsx +432 -0
  305. tools/bolt.diy/app/routes/git.tsx +25 -0
  306. tools/bolt.diy/app/routes/observability.$mandateId.tsx +50 -0
  307. tools/bolt.diy/app/routes/webcontainer.connect.$id.tsx +32 -0
  308. tools/bolt.diy/app/routes/webcontainer.preview.$id.tsx +97 -0
  309. tools/bolt.diy/app/routes/workflow.$proposalId.tsx +170 -0
  310. tools/bolt.diy/app/styles/animations.scss +49 -0
  311. tools/bolt.diy/app/styles/components/code.scss +9 -0
  312. tools/bolt.diy/app/styles/components/editor.scss +135 -0
  313. tools/bolt.diy/app/styles/components/resize-handle.scss +30 -0
  314. tools/bolt.diy/app/styles/components/terminal.scss +3 -0
  315. tools/bolt.diy/app/styles/components/toast.scss +23 -0
  316. tools/bolt.diy/app/styles/diff-view.css +72 -0
  317. tools/bolt.diy/app/styles/index.scss +73 -0
  318. tools/bolt.diy/app/styles/variables.scss +255 -0
  319. tools/bolt.diy/app/styles/z-index.scss +37 -0
  320. tools/bolt.diy/app/types/GitHub.ts +182 -0
  321. tools/bolt.diy/app/types/GitLab.ts +103 -0
  322. tools/bolt.diy/app/types/actions.ts +85 -0
  323. tools/bolt.diy/app/types/artifact.ts +5 -0
  324. tools/bolt.diy/app/types/context.ts +26 -0
  325. tools/bolt.diy/app/types/design-scheme.ts +93 -0
  326. tools/bolt.diy/app/types/global.d.ts +13 -0
  327. tools/bolt.diy/app/types/mandate.ts +333 -0
  328. tools/bolt.diy/app/types/model.ts +25 -0
  329. tools/bolt.diy/app/types/netlify.ts +94 -0
  330. tools/bolt.diy/app/types/supabase.ts +54 -0
  331. tools/bolt.diy/app/types/template.ts +8 -0
  332. tools/bolt.diy/app/types/terminal.ts +9 -0
  333. tools/bolt.diy/app/types/theme.ts +1 -0
  334. tools/bolt.diy/app/types/vercel.ts +67 -0
  335. tools/bolt.diy/app/utils/buffer.ts +29 -0
  336. tools/bolt.diy/app/utils/classNames.ts +65 -0
  337. tools/bolt.diy/app/utils/constants.ts +147 -0
  338. tools/bolt.diy/app/utils/debounce.ts +13 -0
  339. tools/bolt.diy/app/utils/debugLogger.ts +1284 -0
  340. tools/bolt.diy/app/utils/diff.spec.ts +11 -0
  341. tools/bolt.diy/app/utils/diff.ts +117 -0
  342. tools/bolt.diy/app/utils/easings.ts +3 -0
  343. tools/bolt.diy/app/utils/fileLocks.ts +96 -0
  344. tools/bolt.diy/app/utils/fileUtils.ts +121 -0
  345. tools/bolt.diy/app/utils/folderImport.ts +73 -0
  346. tools/bolt.diy/app/utils/formatSize.ts +12 -0
  347. tools/bolt.diy/app/utils/getLanguageFromExtension.ts +24 -0
  348. tools/bolt.diy/app/utils/githubStats.ts +9 -0
  349. tools/bolt.diy/app/utils/gitlabStats.ts +54 -0
  350. tools/bolt.diy/app/utils/logger.ts +162 -0
  351. tools/bolt.diy/app/utils/markdown.ts +155 -0
  352. tools/bolt.diy/app/utils/mobile.ts +4 -0
  353. tools/bolt.diy/app/utils/os.ts +4 -0
  354. tools/bolt.diy/app/utils/path.ts +19 -0
  355. tools/bolt.diy/app/utils/projectCommands.ts +197 -0
  356. tools/bolt.diy/app/utils/promises.ts +19 -0
  357. tools/bolt.diy/app/utils/react.ts +6 -0
  358. tools/bolt.diy/app/utils/sampler.ts +49 -0
  359. tools/bolt.diy/app/utils/selectStarterTemplate.ts +255 -0
  360. tools/bolt.diy/app/utils/shell.ts +384 -0
  361. tools/bolt.diy/app/utils/stacktrace.ts +27 -0
  362. tools/bolt.diy/app/utils/stripIndent.ts +23 -0
  363. tools/bolt.diy/app/utils/terminal.ts +11 -0
  364. tools/bolt.diy/app/utils/unreachable.ts +3 -0
  365. tools/bolt.diy/app/vite-env.d.ts +2 -0
  366. tools/bolt.diy/assets/entitlements.mac.plist +25 -0
  367. tools/bolt.diy/assets/icons/icon.icns +0 -0
  368. tools/bolt.diy/assets/icons/icon.ico +0 -0
  369. tools/bolt.diy/assets/icons/icon.png +0 -0
  370. tools/bolt.diy/bindings.js +78 -0
  371. tools/bolt.diy/bindings.sh +33 -0
  372. tools/bolt.diy/docker-compose.yaml +145 -0
  373. tools/bolt.diy/electron/main/index.ts +201 -0
  374. tools/bolt.diy/electron/main/tsconfig.json +30 -0
  375. tools/bolt.diy/electron/main/ui/menu.ts +29 -0
  376. tools/bolt.diy/electron/main/ui/window.ts +54 -0
  377. tools/bolt.diy/electron/main/utils/auto-update.ts +110 -0
  378. tools/bolt.diy/electron/main/utils/constants.ts +4 -0
  379. tools/bolt.diy/electron/main/utils/cookie.ts +40 -0
  380. tools/bolt.diy/electron/main/utils/reload.ts +35 -0
  381. tools/bolt.diy/electron/main/utils/serve.ts +71 -0
  382. tools/bolt.diy/electron/main/utils/store.ts +3 -0
  383. tools/bolt.diy/electron/main/utils/vite-server.ts +44 -0
  384. tools/bolt.diy/electron/main/vite.config.ts +44 -0
  385. tools/bolt.diy/electron/preload/index.ts +22 -0
  386. tools/bolt.diy/electron/preload/tsconfig.json +7 -0
  387. tools/bolt.diy/electron/preload/vite.config.ts +31 -0
  388. tools/bolt.diy/electron-builder.yml +64 -0
  389. tools/bolt.diy/electron-update.yml +4 -0
  390. tools/bolt.diy/eslint.config.mjs +57 -0
  391. tools/bolt.diy/functions/[[path]].ts +12 -0
  392. tools/bolt.diy/icons/angular.svg +1 -0
  393. tools/bolt.diy/icons/astro.svg +8 -0
  394. tools/bolt.diy/icons/chat.svg +1 -0
  395. tools/bolt.diy/icons/expo-brand.svg +1 -0
  396. tools/bolt.diy/icons/expo.svg +4 -0
  397. tools/bolt.diy/icons/logo-text.svg +1 -0
  398. tools/bolt.diy/icons/logo.svg +4 -0
  399. tools/bolt.diy/icons/mcp.svg +1 -0
  400. tools/bolt.diy/icons/nativescript.svg +1 -0
  401. tools/bolt.diy/icons/netlify.svg +10 -0
  402. tools/bolt.diy/icons/nextjs.svg +1 -0
  403. tools/bolt.diy/icons/nuxt.svg +1 -0
  404. tools/bolt.diy/icons/qwik.svg +1 -0
  405. tools/bolt.diy/icons/react.svg +1 -0
  406. tools/bolt.diy/icons/remix.svg +24 -0
  407. tools/bolt.diy/icons/remotion.svg +1 -0
  408. tools/bolt.diy/icons/shadcn.svg +21 -0
  409. tools/bolt.diy/icons/slidev.svg +60 -0
  410. tools/bolt.diy/icons/solidjs.svg +1 -0
  411. tools/bolt.diy/icons/stars.svg +1 -0
  412. tools/bolt.diy/icons/svelte.svg +1 -0
  413. tools/bolt.diy/icons/typescript.svg +1 -0
  414. tools/bolt.diy/icons/vite.svg +1 -0
  415. tools/bolt.diy/icons/vue.svg +1 -0
  416. tools/bolt.diy/load-context.ts +9 -0
  417. tools/bolt.diy/notarize.cjs +31 -0
  418. tools/bolt.diy/package.json +218 -0
  419. tools/bolt.diy/playwright.config.preview.ts +35 -0
  420. tools/bolt.diy/pre-start.cjs +26 -0
  421. tools/bolt.diy/public/apple-touch-icon-precomposed.png +0 -0
  422. tools/bolt.diy/public/apple-touch-icon.png +0 -0
  423. tools/bolt.diy/public/favicon.ico +0 -0
  424. tools/bolt.diy/public/favicon.svg +4 -0
  425. tools/bolt.diy/public/icons/AmazonBedrock.svg +1 -0
  426. tools/bolt.diy/public/icons/Anthropic.svg +4 -0
  427. tools/bolt.diy/public/icons/Cohere.svg +4 -0
  428. tools/bolt.diy/public/icons/Deepseek.svg +5 -0
  429. tools/bolt.diy/public/icons/Default.svg +4 -0
  430. tools/bolt.diy/public/icons/Google.svg +4 -0
  431. tools/bolt.diy/public/icons/Groq.svg +4 -0
  432. tools/bolt.diy/public/icons/HuggingFace.svg +4 -0
  433. tools/bolt.diy/public/icons/Hyperbolic.svg +3 -0
  434. tools/bolt.diy/public/icons/LMStudio.svg +5 -0
  435. tools/bolt.diy/public/icons/Mistral.svg +4 -0
  436. tools/bolt.diy/public/icons/Ollama.svg +4 -0
  437. tools/bolt.diy/public/icons/OpenAI.svg +4 -0
  438. tools/bolt.diy/public/icons/OpenAILike.svg +4 -0
  439. tools/bolt.diy/public/icons/OpenRouter.svg +4 -0
  440. tools/bolt.diy/public/icons/Perplexity.svg +4 -0
  441. tools/bolt.diy/public/icons/Together.svg +4 -0
  442. tools/bolt.diy/public/icons/xAI.svg +5 -0
  443. tools/bolt.diy/public/inspector-script.js +292 -0
  444. tools/bolt.diy/public/logo-dark-styled.png +0 -0
  445. tools/bolt.diy/public/logo-dark.png +0 -0
  446. tools/bolt.diy/public/logo-light-styled.png +0 -0
  447. tools/bolt.diy/public/logo-light.png +0 -0
  448. tools/bolt.diy/public/logo.svg +15 -0
  449. tools/bolt.diy/public/social_preview_index.jpg +0 -0
  450. tools/bolt.diy/scripts/clean.js +45 -0
  451. tools/bolt.diy/scripts/electron-dev.mjs +181 -0
  452. tools/bolt.diy/scripts/setup-env.sh +41 -0
  453. tools/bolt.diy/scripts/update-imports.sh +7 -0
  454. tools/bolt.diy/scripts/update.sh +52 -0
  455. tools/bolt.diy/services/execution-governor/Dockerfile +41 -0
  456. tools/bolt.diy/services/execution-governor/config.ts +42 -0
  457. tools/bolt.diy/services/execution-governor/index.ts +683 -0
  458. tools/bolt.diy/services/execution-governor/metrics.ts +141 -0
  459. tools/bolt.diy/services/execution-governor/package.json +31 -0
  460. tools/bolt.diy/services/execution-governor/priority-queue.ts +139 -0
  461. tools/bolt.diy/services/execution-governor/tsconfig.json +21 -0
  462. tools/bolt.diy/services/execution-governor/types.ts +145 -0
  463. tools/bolt.diy/services/headless-executor/Dockerfile +43 -0
  464. tools/bolt.diy/services/headless-executor/executor.ts +210 -0
  465. tools/bolt.diy/services/headless-executor/index.ts +323 -0
  466. tools/bolt.diy/services/headless-executor/package.json +27 -0
  467. tools/bolt.diy/services/headless-executor/tsconfig.json +21 -0
  468. tools/bolt.diy/services/headless-executor/types.ts +38 -0
  469. tools/bolt.diy/test-workflows.sh +240 -0
  470. tools/bolt.diy/tests/integration/corporate-swarm.test.ts +208 -0
  471. tools/bolt.diy/tests/mandates/budget-limited.json +34 -0
  472. tools/bolt.diy/tests/mandates/complex.json +53 -0
  473. tools/bolt.diy/tests/mandates/constraint-enforced.json +36 -0
  474. tools/bolt.diy/tests/mandates/simple.json +35 -0
  475. tools/bolt.diy/tsconfig.json +37 -0
  476. tools/bolt.diy/types/istextorbinary.d.ts +15 -0
  477. tools/bolt.diy/uno.config.ts +279 -0
  478. tools/bolt.diy/vite-electron.config.ts +76 -0
  479. tools/bolt.diy/vite.config.ts +112 -0
  480. tools/bolt.diy/worker-configuration.d.ts +22 -0
  481. tools/bolt.diy/wrangler.toml +6 -0
  482. tools/code_generator.py +461 -0
  483. tools/file_operations.py +465 -0
  484. tools/mandate_generator.py +337 -0
  485. tools/mcpClientUtils.py +1216 -0
  486. tools/sensors.py +285 -0
  487. utils/Agent_types.py +15 -0
  488. utils/AnyToStr.py +0 -0
  489. utils/HHCS.py +277 -0
  490. utils/__init__.py +30 -0
  491. utils/agent.py +3627 -0
  492. utils/aop.py +2948 -0
  493. utils/canonical.py +143 -0
  494. utils/conversation.py +1195 -0
  495. utils/doctrine_versioning +230 -0
  496. utils/formatter.py +474 -0
  497. utils/ledger.py +311 -0
  498. utils/out_types.py +16 -0
  499. utils/rollback.py +339 -0
  500. utils/router.py +929 -0
  501. utils/tui.py +1908 -0
branches/CRCA-Q.py ADDED
@@ -0,0 +1,2728 @@
1
+ import os,csv,time,random,json,re,threading,asyncio,hmac,hashlib,base64,subprocess
2
+ import sys
3
+ from typing import Dict,Optional,Any,List,Tuple,Union,Callable
4
+ from datetime import datetime,timedelta
5
+ from collections import defaultdict
6
+ from pathlib import Path
7
+ from urllib.parse import urlencode
8
+ import requests,pandas as pd,numpy as np
9
+ from loguru import logger
10
+ from dotenv import load_dotenv
11
+ from swarms import Agent
12
+
13
+ # Add parent directory to path for importing CRCA
14
+ _parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15
+ if _parent_dir not in sys.path:
16
+ sys.path.insert(0, _parent_dir)
17
+
18
+ from CRCA import CRCAAgent
19
+ try:from rich.console import Console;from rich.table import Table;from rich.panel import Panel;from rich.columns import Columns;from rich.layout import Layout;from rich.text import Text;from rich import box;from rich.align import Align;from rich.progress import Progress,SpinnerColumn,TextColumn,BarColumn,TimeRemainingColumn;RICH_AVAILABLE=True
20
+ except ImportError:RICH_AVAILABLE=False;Console=None
21
+ DAYS_BACK=90
22
+ AUTO_MULTI_ASSET=True
23
+ MAX_ASSETS_LIMIT=10
24
+ MIN_ASSET_VALUE=1.5
25
+ VERBOSE_LOGGING=False
26
+ PRIORITY_WEIGHTS={'predicted_return':.45,'volume':.15,'volatility':.15,'trend_strength':.1,'signal_quality':.1,'market_cap':.05}
27
+ ROTATION_ENABLED=True
28
+ ROTATION_MIN_SCORE_DIFF=.1
29
+ ROTATION_MAX_CHANGES_PER_CYCLE=10
30
+ BATCH_SIZE=100
31
+ BATCH_DELAY=1.
32
+ LONGTERM_MODE_ENABLED=False
33
+ TRADING_CONFIG={'account_size':10000,'account_category':'medium','max_position_size':.3,'max_position_hard_cap':.3,'min_trade_value':5.,'position_size_multiplier':1.,'conservative_mode':True,'aggressive_mode':False,'cooldown_enabled':False,'cooldown_loop_trigger':25,'cooldown_api_budget_threshold':.3,'cooldown_congestion_threshold':.65,'cooldown_min_loops':3,'cooldown_max_loops':4,'cooldown_sleep_multiplier':1.15,'stop_loss_pct':-1e1,'stop_gain_pct':2e1,'promotion_threshold_pct':.1,'promotion_liquidate_enabled':False,'promotion_debounce_secs':5.,'quote_preferences':{'kraken':['EUR','USD','USDC','USDT'],'binance':['USDT','BUSD','USD','USDC'],'coinbase':['USD','USDC','USDT'],'default':['USDT','USD','USDC']}}
34
+ LONGTERM_MODE_CONFIG={'prediction_horizon_days':7,'position_evaluation_interval_hours':1,'max_position_size':.005,'min_confidence_threshold':.85,'min_asset_price_usd':.0,'max_asset_price_usd':1.,'position_size_multiplier':.1,'loop_interval_seconds':3600,'full_refresh_interval_hours':24,'use_crca_agent_heavily':True,'crca_max_loops':5,'min_trade_value':1.,'conservative_mode':True,'aggressive_mode':False}
35
+ TOP_CRYPTO_SYMBOLS='BTC','ETH','BNB','SOL','XRP','ADA','DOGE','TRX','DOT','MATIC','AVAX','LINK','UNI','ATOM','LTC','ETC','XLM','ALGO','VET','ICP','FIL','AAVE','EOS','THETA','XTZ','EGLD','HBAR','NEAR','QNT','FLOW','SAND','MANA','AXS','GALA','ENJ','CHZ','BAT','ZEC','DASH','WAVES','MKR','COMP','SNX','YFI','SUSHI','CRV','1INCH','RUNE','LUNA','UST','APT','ARB','OP','INJ','TIA','IMX','SUI','SEI','RENDER','FET','GRT','RNDR','LDO','AR','STX','FTM','PEPE','SHIB','FLOKI','BONK','WIF','BOME','MYRO','POPCAT','MEW','MEME','JTO','PYTH','WLD','ONDO','JUP','RAY','ORCA','MNGO','STEP','COPE','HNT','IOTX','RAD','BAND','CTSI','ADX','AUCTION','DAR','BNX','RGT','MOVR','CITY','ENS','KP3R','QI','BADGER','FIS','OM','POND','DEGO','ALICE','LINA','PERP','RAMP','SUPER','CFX','EPS','AUTO','TKO','PROM','GMX','MAGIC','GFT','HFT','FXS','HOOK','MAG','HIFI','FRONT','CVP','AGLD','BETA','RARE','LAZIO'
36
+ if TRADING_CONFIG['account_size']<100:TRADING_CONFIG.update({'account_category':'micro','max_position_size':.18,'position_size_multiplier':.5,'conservative_mode':True})
37
+ elif TRADING_CONFIG['account_size']<1000:TRADING_CONFIG.update({'account_category':'small','max_position_size':.2,'position_size_multiplier':.8})
38
+ elif TRADING_CONFIG['account_size']<10000:TRADING_CONFIG.update({'account_category':'medium','max_position_size':.3,'position_size_multiplier':1.})
39
+ else:TRADING_CONFIG.update({'account_category':'large','max_position_size':.5,'position_size_multiplier':1.})
40
+ ALTERNATIVE_DATA_CONFIG={'use_real_apis':True,'cache_type':'redis','window_size_days':7,'api_keys':{'twitter':None,'newsapi':None,'etherscan':None,'thegraph':None},'enabled_sources':{'onchain':True,'social':True,'news':True,'github':True,'exchange':True},'cache_ttl':{'onchain':7200,'social':1800,'news':3600,'github':7200,'exchange':1800},'github_repos':['ethereum/go-ethereum','ethereum/consensus-specs','ethereum/execution-specs'],'exchange_metrics':{'exchanges':['binance','bybit'],'symbols':['ETH/USDT','BTC/USDT']},'confidence_weights':{'freshness':.4,'reliability':.4,'stability':.2}}
41
+ load_dotenv()
42
+ TWITTER_API_FALLBACK='TWITTER_BEARER_TOKEN'
43
+ NEWSAPI_FALLBACK=None
44
+ ETHERSCAN_FALLBACK='ETHERSCAN_API_KEY'
45
+ THEGRAPH_FALLBACK='THEGRAPH_API_KEY'
46
+ ALTERNATIVE_DATA_CONFIG['api_keys']['twitter']=os.getenv('TWITTER_BEARER_TOKEN')or os.getenv('TWITTER_API_KEY')or TWITTER_API_FALLBACK
47
+ ALTERNATIVE_DATA_CONFIG['api_keys']['newsapi']=os.getenv('NEWSAPI_KEY')or NEWSAPI_FALLBACK
48
+ ALTERNATIVE_DATA_CONFIG['api_keys']['etherscan']=os.getenv('ETHERSCAN_API_KEY')or ETHERSCAN_FALLBACK
49
+ ALTERNATIVE_DATA_CONFIG['api_keys']['thegraph']=os.getenv('THEGRAPH_API_KEY')or THEGRAPH_FALLBACK
50
+ KRAKEN_API_KEY_FALLBACK='KRAKEN_API_KEY'
51
+ KRAKEN_API_SECRET_FALLBACK='KRAKEN_API_SECRET'
52
+ KRAKEN_API_PASSPHRASE_FALLBACK=None
53
+ try:import ccxt;CCXT_AVAILABLE=True
54
+ except ImportError:CCXT_AVAILABLE=False
55
+ LIVE_TRADING_MODE_DEFAULT=False
56
+ try:from web3 import Web3
57
+ except ImportError:pass
58
+ try:from numba import jit;NUMBA_AVAILABLE=True
59
+ except ImportError:
60
+ NUMBA_AVAILABLE=False
61
+ def jit(*args,**kwargs):
62
+ def decorator(func):return func
63
+ return decorator
64
+ try:import dowhy;from dowhy import CausalModel;DOWHY_AVAILABLE=True
65
+ except ImportError:DOWHY_AVAILABLE=False
66
+ ECONML_AVAILABLE=False
67
+ CAUSALML_AVAILABLE=False
68
+ try:import torch,torch.nn as nn;TORCH_AVAILABLE=True
69
+ except ImportError:TORCH_AVAILABLE=False
70
+ try:import cvxpy as cp;CVXPY_AVAILABLE=True
71
+ except ImportError:CVXPY_AVAILABLE=False
72
+ try:import optuna;OPTUNA_AVAILABLE=True
73
+ except ImportError:OPTUNA_AVAILABLE=False
74
+ try:import yfinance as yf;YFINANCE_AVAILABLE=True
75
+ except ImportError:YFINANCE_AVAILABLE=False
76
+ try:import websockets;WEBSOCKETS_AVAILABLE=True
77
+ except ImportError:WEBSOCKETS_AVAILABLE=False
78
+ try:from sklearn.ensemble import RandomForestRegressor,GradientBoostingRegressor;from sklearn.linear_model import LinearRegression;SKLEARN_AVAILABLE=True
79
+ except ImportError:SKLEARN_AVAILABLE=False
80
+ try:import xgboost as xgb;XGBOOST_AVAILABLE=True
81
+ except ImportError:XGBOOST_AVAILABLE=False
82
+ try:import lightgbm as lgb;LIGHTGBM_AVAILABLE=True
83
+ except ImportError:LIGHTGBM_AVAILABLE=False
84
+ try:import redis;REDIS_AVAILABLE=True
85
+ except ImportError:REDIS_AVAILABLE=False
86
+ class AssetDiscovery:
87
+ def __init__(self,wallet_value:float,min_asset_value:float=1e1,max_assets:int=50,longterm_mode:bool=False,max_asset_price_usd:Optional[float]=None):
88
+ self.wallet_value=wallet_value;self.longterm_mode=longterm_mode;self.max_asset_price_usd=max_asset_price_usd
89
+ if longterm_mode:self.min_asset_value=.1;self.max_assets=max(max_assets,100)
90
+ else:self.min_asset_value=min_asset_value;self.max_assets=max_assets
91
+ self.top_crypto_symbols=list(TOP_CRYPTO_SYMBOLS)
92
+ def discover_assets(self)->List[Dict[str,str]]:
93
+ max_affordable=int(self.wallet_value/self.min_asset_value);num_assets=min(self.max_assets,max_affordable,len(self.top_crypto_symbols))
94
+ if num_assets<=0:return[]
95
+ return[{'symbol':sym,'type':'crypto'}for sym in self.top_crypto_symbols[:num_assets]]
96
+ def get_asset_priority_score(self,symbol:str,price_data:pd.DataFrame,signal_scores:Dict[str,Dict[str,float]],current_price:float,predicted_return:Optional[float]=None,prediction_confidence:Optional[float]=None,prediction_uncertainty:Optional[float]=None)->float:
97
+ if price_data.empty:return .0
98
+ if self.longterm_mode and self.max_asset_price_usd is not None and current_price>self.max_asset_price_usd:return .0
99
+ scores={}
100
+ if'volume'in price_data.columns:avg_volume=price_data['volume'].tail(30).mean();max_volume=price_data['volume'].max();scores['volume']=min(1.,avg_volume/max_volume)if max_volume>0 else .0
101
+ else:scores['volume']=.5
102
+ if'returns'in price_data.columns:volatility=price_data['returns'].tail(30).std()*np.sqrt(252);scores['volatility']=min(1.,max(.0,(volatility-.2)/.6))if volatility>0 else .0
103
+ else:scores['volatility']=.5
104
+ if'price'in price_data.columns:
105
+ prices=price_data['price'].tail(30)
106
+ if len(prices)>=10:short_ma,long_ma=prices.tail(10).mean(),prices.mean();trend_strength=abs((short_ma-long_ma)/long_ma)if long_ma>0 else .0;scores['trend_strength']=min(1.,trend_strength*10)
107
+ else:scores['trend_strength']=.5
108
+ else:scores['trend_strength']=.5
109
+ signal_quality=.0
110
+ if signal_scores:
111
+ signal_values=[v.get('score',.0)for v in signal_scores.values()if isinstance(v,dict)]
112
+ if signal_values:signal_quality=np.mean(signal_values)
113
+ scores['signal_quality']=signal_quality
114
+ try:market_cap_rank=self.top_crypto_symbols.index(symbol)if symbol in self.top_crypto_symbols else 100;scores['market_cap']=max(.0,1.-market_cap_rank/100)
115
+ except ValueError:scores['market_cap']=.3
116
+ if self.longterm_mode and current_price>0:
117
+ if current_price<1.:price_bonus=1.+(1.-current_price)*.5;scores['price_bonus']=price_bonus
118
+ else:scores['price_bonus']=1.
119
+ else:scores['price_bonus']=1.
120
+ if predicted_return is not None:
121
+ normalized_return=min(1.,max(.0,(predicted_return+.5)/1.))
122
+ if prediction_confidence is not None:normalized_return*=prediction_confidence
123
+ if prediction_uncertainty is not None:normalized_return*=max(.3,1.-min(1.,prediction_uncertainty))
124
+ scores['predicted_return']=normalized_return
125
+ elif'price'in price_data.columns and len(price_data)>=7:recent_return=price_data['price'].iloc[-1]/price_data['price'].iloc[-7]-1 if price_data['price'].iloc[-7]>0 else .0;scores['predicted_return']=min(1.,max(.0,(recent_return+.5)/1.))
126
+ else:scores['predicted_return']=.5
127
+ final_score=float(np.clip(sum(PRIORITY_WEIGHTS.get(factor,.0)*score for(factor,score)in scores.items()if factor!='price_bonus'),.0,1.))
128
+ if'price_bonus'in scores:final_score=min(1.,final_score*scores['price_bonus'])
129
+ return final_score
130
+ def sort_assets_by_priority(self,assets:List[Dict[str,str]],price_data_dict:Dict[str,pd.DataFrame],signal_scores_dict:Dict[str,Dict[str,Dict[str,float]]],current_prices:Dict[str,float])->List[Dict[str,str]]:
131
+ asset_scores=[]
132
+ for asset in assets:
133
+ symbol=asset.get('symbol','').upper()
134
+ if symbol not in price_data_dict:continue
135
+ priority_score=self.get_asset_priority_score(symbol,price_data_dict[symbol],signal_scores_dict.get(symbol,{}),current_prices.get(symbol,.0));asset_scores.append((priority_score,asset))
136
+ asset_scores.sort(key=lambda x:x[0],reverse=True);return[asset for(_,asset)in asset_scores]
137
+ class ExchangeAssetCatalog:
138
+ CATALOG={'kraken':['BTC','ETH','SOL','XRP','ADA','DOT','DOGE','AVAX','LINK','LTC','MATIC','ATOM','UNI','XLM','FIL','AAVE','ETC','EGLD','ALGO','FTM','HBAR','NEAR','SAND','MANA','ICP','GRT','APE','CRV','KAVA','SNX','QTUM','BCH','MINA','XMR','ZEC','COMP','1INCH','REN','WAVES','TRX','XTZ','EOS','SUSHI','YFI','RUNE','DASH','OMG','ZRX','BAT','ENJ','ANKR'],'binance':['BTC','ETH','BNB','SOL','XRP','ADA','DOGE','TRX','DOT','MATIC','AVAX','LINK','UNI','ATOM','LTC','ETC','XLM','ALGO','VET','ICP','FIL','AAVE','EGLD','HBAR','NEAR','SAND','MANA','AXS','GALA','ENJ','CHZ','BAT','ZEC','DASH','WAVES','MKR','COMP','SNX','YFI','SUSHI','CRV','1INCH','RUNE','FET','LDO','AR','STX','FTM','APT','ARB','OP','INJ','TIA','IMX','SUI','SEI','RNDR','PYTH','WLD','ONDO','JUP','PEPE','SHIB'],'coinbase':['BTC','ETH','SOL','XRP','ADA','DOT','DOGE','AVAX','LINK','LTC','MATIC','ATOM','UNI','XLM','AAVE','FIL','ETC','ALGO','FTM','NEAR','SAND','MANA','ICP','GRT','APE','CRV','SNX','BCH','XMR','COMP','1INCH','TRX','XTZ','EOS','SUSHI','YFI','RUNE','ZRX','BAT','ENJ','ANKR','ARB','OP','INJ','TIA','IMX','SUI','RNDR','PYTH','WLD','PEPE','SHIB']}
139
+ @classmethod
140
+ def default_assets(cls,exchange:str,max_assets:int)->List[Dict[str,str]]:symbols=cls.CATALOG.get((exchange or'').lower(),[]);return[{'symbol':sym,'type':'crypto'}for sym in symbols[:max_assets]]
141
+ class AssetRotationManager:
142
+ def __init__(self,min_score_diff:float=ROTATION_MIN_SCORE_DIFF,max_changes_per_cycle:int=ROTATION_MAX_CHANGES_PER_CYCLE):self.min_score_diff=min_score_diff;self.max_changes_per_cycle=max_changes_per_cycle
143
+ def calculate_rotation_score(self,asset:Dict[str,str],prediction:Optional[float],signal_scores:Dict[str,Dict[str,float]],current_price:float,prediction_confidence:Optional[float]=None,prediction_uncertainty:Optional[float]=None)->float:
144
+ if prediction is None:return .0
145
+ score=min(1.,max(.0,(prediction+.5)/1.))
146
+ if prediction_confidence is not None:score*=.5+.5*prediction_confidence
147
+ if prediction_uncertainty is not None:score*=max(.4,1.-min(1.,prediction_uncertainty))
148
+ if signal_scores:
149
+ signal_values=[v.get('score',.0)for v in signal_scores.values()if isinstance(v,dict)]
150
+ if signal_values:score=score*.7+np.mean(signal_values)*.3
151
+ return float(np.clip(score,.0,1.))
152
+ def evaluate_asset_rotation(self,current_positions:Dict[str,Dict[str,Any]],all_assets:List[Dict[str,str]],predictions:Dict[str,float],signal_scores_dict:Dict[str,Dict[str,Dict[str,float]]],current_prices:Dict[str,float],prediction_confidences:Dict[str,float],prediction_uncertainties:Dict[str,float],portfolio_value:float)->Dict[str,Any]:
153
+ if not ROTATION_ENABLED:return{'exits':[],'entries':[]}
154
+ asset_scores={}
155
+ for asset in all_assets:
156
+ symbol=asset.get('symbol','').upper()
157
+ if symbol not in predictions:continue
158
+ asset_scores[symbol]=self.calculate_rotation_score(asset=asset,prediction=predictions.get(symbol),signal_scores=signal_scores_dict.get(symbol,{}),current_price=current_prices.get(symbol,.0),prediction_confidence=prediction_confidences.get(symbol),prediction_uncertainty=prediction_uncertainties.get(symbol))
159
+ exits=[{'symbol':symbol,'score':asset_scores.get(symbol,.0),'position':position}for(symbol,position)in current_positions.items()if asset_scores.get(symbol,.0)<.3];current_symbols=set(current_positions.keys());potential_entries=[{'symbol':symbol,'score':score,'predicted_return':predictions.get(symbol,.0)}for(symbol,score)in asset_scores.items()if symbol not in current_symbols and score>.6];potential_entries.sort(key=lambda x:x['score'],reverse=True);max_exits=min(len(exits),self.max_changes_per_cycle//2);max_entries=min(len(potential_entries),self.max_changes_per_cycle-max_exits);return{'exits':exits[:max_exits],'entries':potential_entries[:max_entries]}
160
+ class MarketDataClient:
161
+ def __init__(self,demo_mode:bool=True):self.demo_mode=demo_mode;self.price_data=pd.DataFrame()
162
+ def fetch_price_data(self,asset:str='ethereum',days_back:int=364,vs_currency:str='usd',asset_type:str='auto')->pd.DataFrame:
163
+ if self.demo_mode:return self._generate_demo_data(days_back,asset)
164
+ if asset_type=='auto':asset_type=self._detect_asset_type(asset)
165
+ if asset_type=='crypto'or asset.lower()in['ethereum','eth','bitcoin','btc']:return self._fetch_crypto_data(asset,days_back,vs_currency)
166
+ if asset_type=='stock'and YFINANCE_AVAILABLE:
167
+ try:return self._fetch_stock_data(asset,days_back)
168
+ except Exception:pass
169
+ if asset_type=='fx':return self._fetch_fx_data(asset,days_back)
170
+ if asset_type=='futures':return self._fetch_futures_data(asset,days_back)
171
+ return pd.DataFrame()
172
+ def _detect_asset_type(self,asset:str)->str:
173
+ asset_lower=asset.lower()
174
+ if asset_lower in['eth','btc','ethereum','bitcoin','usdt','usdc']or len(asset)<=5:return'crypto'
175
+ if len(asset)==6 and asset_lower.isalpha():return'fx'
176
+ return'stock'
177
+ def _fetch_fx_data(self,pair:str,days_back:int)->pd.DataFrame:
178
+ try:
179
+ if YFINANCE_AVAILABLE:
180
+ fx_symbol=f"{pair.upper()}=X"if'='not in pair else pair.upper();end_date,start_date=datetime.now(),datetime.now()-timedelta(days=days_back);df=yf.Ticker(fx_symbol).history(start=start_date,end=end_date)
181
+ if not df.empty:df.reset_index(inplace=True);df.rename(columns={'Date':'date','Close':'price','Volume':'volume'},inplace=True);df['market_cap']=0;return df[['date','price','volume','market_cap']]
182
+ except Exception:pass
183
+ return pd.DataFrame()
184
+ def _fetch_futures_data(self,symbol:str,days_back:int)->pd.DataFrame:
185
+ try:
186
+ if YFINANCE_AVAILABLE:
187
+ futures_symbol=f"{symbol.upper()}=F"if'='not in symbol else symbol.upper();end_date,start_date=datetime.now(),datetime.now()-timedelta(days=days_back);df=yf.Ticker(futures_symbol).history(start=start_date,end=end_date)
188
+ if not df.empty:df.reset_index(inplace=True);df.rename(columns={'Date':'date','Close':'price','Volume':'volume'},inplace=True);df['market_cap']=0;return df[['date','price','volume','market_cap']]
189
+ except Exception:pass
190
+ return pd.DataFrame()
191
+ def _get_asset_base_price(self,asset:str)->float:
192
+ asset_upper,asset_lower=asset.upper(),asset.lower();crypto_prices={'BTC':65e3,'BITCOIN':65e3,'ETH':35e2,'ETHEREUM':35e2,'DOGE':.15,'DOGECOIN':.15,'XRP':.75,'RIPPLE':.75,'DOT':7.,'POLKADOT':7.,'MATIC':.75,'POLYGON':.75,'ADA':.5,'CARDANO':.5,'SOL':1e2,'SOLANA':1e2,'LINK':15.,'CHAINLINK':15.,'UNI':8.,'UNISWAP':8.,'AVAX':35.,'AVALANCHE':35.,'ATOM':1e1,'COSMOS':1e1,'ALGO':.2,'ALGORAND':.2,'TRX':.1,'TRON':.1,'LTC':8e1,'LITECOIN':8e1,'BCH':3e2,'BITCOINCASH':3e2}
193
+ if asset_upper in crypto_prices:return crypto_prices[asset_upper]
194
+ if asset_lower in crypto_prices:return crypto_prices[asset_lower]
195
+ for(key,price)in crypto_prices.items():
196
+ if key.lower()in asset_lower or asset_lower in key.lower():return price
197
+ return 1e3 if len(asset)<=3 else 1e1 if len(asset)<=5 else 1.
198
+ def _get_asset_volatility_range(self,asset:str)->tuple:
199
+ asset_upper=asset.upper()
200
+ if any(hv in asset_upper for hv in['DOGE','SHIB','PEPE','FLOKI']):return .05,.15
201
+ if any(mv in asset_upper for mv in['XRP','ADA','DOT','MATIC','ALGO']):return .03,.08
202
+ if any(lv in asset_upper for lv in['BTC','ETH','USDT','USDC']):return .02,.05
203
+ return .03,.08
204
+ def _generate_demo_data(self,days_back:int,asset:str='ethereum')->pd.DataFrame:
205
+ dates=pd.date_range(end=datetime.now(),periods=days_back,freq='D');base_price=self._get_asset_base_price(asset);vol_min,vol_max=self._get_asset_volatility_range(asset);daily_vol=random.uniform(vol_min,vol_max);price_trend=np.linspace(0,base_price*.15,days_back);noise_std=base_price*daily_vol;noise=np.random.normal(0,noise_std,days_back);prices=base_price+price_trend+noise
206
+ for i in range(1,len(prices)):
207
+ if random.random()<.1:prices[i]+=base_price*random.uniform(-.1,.1)
208
+ prices=np.maximum(prices,base_price*.1);volume_base=base_price*1000;volumes=np.random.uniform(volume_base*.5,volume_base*2.,days_back);supply_estimates={'BTC':19000000,'ETH':120000000,'DOGE':140000000000,'XRP':54000000000,'DOT':1200000000,'MATIC':9000000000};supply=supply_estimates.get(asset.upper(),base_price*1000000000/max(base_price,1.));return pd.DataFrame({'date':dates,'price':prices,'volume':volumes,'market_cap':prices*supply})
209
+ def _fetch_crypto_data(self,asset:str,days_back:int,vs_currency:str)->pd.DataFrame:
210
+ try:
211
+ coin_id='ethereum'if asset.lower()in['ethereum','eth']else'bitcoin';end_date,start_date=datetime.now(),datetime.now()-timedelta(days=days_back);response=requests.get(f"https://api.coingecko.com/api/v3/coins/{coin_id}/market_chart/range",params={'vs_currency':vs_currency,'from':int(start_date.timestamp()),'to':int(end_date.timestamp())},timeout=10)
212
+ if response.status_code==200:data=response.json();prices,volumes,market_caps=data.get('prices',[]),data.get('total_volumes',[]),data.get('market_caps',[]);df_data=[{'date':datetime.fromtimestamp(timestamp/1000),'price':price,'volume':volumes[i][1]if i<len(volumes)else 0,'market_cap':market_caps[i][1]if i<len(market_caps)else 0}for(i,(timestamp,price))in enumerate(prices)];return pd.DataFrame(df_data)
213
+ except Exception:pass
214
+ return pd.DataFrame()
215
+ def _fetch_stock_data(self,symbol:str,days_back:int)->pd.DataFrame:
216
+ try:end_date,start_date=datetime.now(),datetime.now()-timedelta(days=days_back);ticker=yf.Ticker(symbol);df=ticker.history(start=start_date,end=end_date);df.reset_index(inplace=True);df.rename(columns={'Date':'date','Close':'price','Volume':'volume'},inplace=True);df['market_cap']=df['price']*ticker.info.get('sharesOutstanding',0);return df[['date','price','volume','market_cap']]
217
+ except Exception:pass
218
+ return pd.DataFrame()
219
+ def fetch_correlation(self,asset1:str,asset2:str,price_data1:pd.DataFrame,window:int=14)->pd.Series:
220
+ if price_data1.empty or'returns'not in price_data1.columns:return pd.Series()
221
+ try:
222
+ price_data2=self.fetch_price_data(asset2,len(price_data1))
223
+ if price_data2.empty:return pd.Series()
224
+ price_data2['returns']=price_data2['price'].pct_change();aligned=price_data1.set_index('date').join(price_data2.set_index('date')[['returns']],rsuffix='_2',how='left');aligned['returns_2']=aligned['returns_2'].ffill().bfill();return aligned['returns'].rolling(window=window,min_periods=5).corr(aligned['returns_2'])
225
+ except Exception:return pd.Series(np.zeros(len(price_data1)))
226
+ def fetch_multiple_assets(self,assets:List[Dict[str,str]],days_back:int)->Dict[str,pd.DataFrame]:
227
+ if not assets:return{}
228
+ result={}
229
+ for asset_config in assets:
230
+ symbol=asset_config.get('symbol','')
231
+ if not symbol:continue
232
+ try:
233
+ df=self.fetch_price_data(asset=symbol,days_back=days_back,vs_currency=asset_config.get('vs_currency','usd'),asset_type=asset_config.get('type','auto'))
234
+ if df.empty:df=self._generate_demo_data(days_back,asset=symbol)
235
+ if not df.empty:
236
+ if'returns'not in df.columns:df['returns']=df['price'].pct_change()
237
+ result[symbol.upper()]=df
238
+ except Exception:
239
+ df=self._generate_demo_data(days_back,asset=symbol)
240
+ if not df.empty:df['returns']=df['price'].pct_change();result[symbol.upper()]=df
241
+ return result
242
+ def validate_unified_schema(self,data_dict:Dict[str,pd.DataFrame])->Dict[str,pd.DataFrame]:
243
+ if not data_dict:return{}
244
+ required_cols=['date','price','volume'];validated,price_ranges={},{}
245
+ for(symbol,df)in data_dict.items():
246
+ if df.empty:continue
247
+ if any(col not in df.columns for col in required_cols):continue
248
+ if not pd.api.types.is_datetime64_any_dtype(df['date']):df['date']=pd.to_datetime(df['date'])
249
+ if'price'in df.columns and len(df)>0:price_ranges[symbol]={'min':df['price'].min(),'max':df['price'].max(),'mean':df['price'].mean(),'std':df['price'].std()}
250
+ if'returns'not in df.columns:df=df.copy();df['returns']=df['price'].pct_change()
251
+ validated[symbol]=df
252
+ if len(price_ranges)>1:
253
+ price_means=[r['mean']for r in price_ranges.values()]
254
+ if np.mean(price_means)>0 and np.std(price_means)/np.mean(price_means)<.1 and np.mean(price_means)>100:0
255
+ if len(validated)<2:return validated
256
+ all_dates=set()
257
+ for df in validated.values():all_dates.update(df['date'].dt.date)
258
+ if not all_dates:return validated
259
+ aligned_dates=pd.date_range(start=min(all_dates),end=max(all_dates),freq='D');aligned_dict={}
260
+ for(symbol,df)in validated.items():
261
+ df_aligned=df.set_index('date').reindex(aligned_dates,method='ffill');df_aligned.reset_index(inplace=True);df_aligned.rename(columns={'index':'date'},inplace=True);df_aligned['price']=df_aligned['price'].ffill().bfill();df_aligned['volume']=df_aligned['volume'].fillna(0)
262
+ if'returns'in df_aligned.columns:df_aligned['returns']=df_aligned['returns'].fillna(0)
263
+ aligned_dict[symbol]=df_aligned
264
+ return aligned_dict
265
+ def compute_multi_asset_covariance(self,returns_dict:Dict[str,pd.Series],window:int=20)->pd.DataFrame:
266
+ if not returns_dict or len(returns_dict)<2:return pd.DataFrame()
267
+ returns_df=pd.DataFrame(returns_dict).dropna();return returns_df.cov()if len(returns_df)>=window else returns_df.cov()
268
+ class ExchangeSocketManager:
269
+ def __init__(self):self.connections={};self.callbacks={};self.reconnect_attempts={};self.max_reconnect_attempts=10;self.reconnect_delay=1.;self.order_books={};self.running={};self.last_message_time={}
270
+ async def connect(self,exchange:str,symbol:str,callback:callable):
271
+ if not WEBSOCKETS_AVAILABLE:return False
272
+ key=f"{exchange}:{symbol}";self.callbacks[key]=callback;self.reconnect_attempts[key]=0
273
+ try:
274
+ if exchange.lower()=='coinbase':return await self._connect_coinbase(symbol,callback)
275
+ elif exchange.lower()=='binance':return await self._connect_binance(symbol,callback)
276
+ else:return False
277
+ except Exception:return await self._reconnect(exchange,symbol,callback)
278
+ async def _connect_coinbase(self,symbol:str,callback:callable)->bool:
279
+ try:
280
+ import websockets,json;key,product_id=f"coinbase:{symbol}",f"{symbol.upper()}-USD"
281
+ if key not in self.order_books:self.order_books[key]={'bids':{},'asks':{},'sequence':0}
282
+ async def _handle_message(ws,key):
283
+ async for message in ws:
284
+ try:
285
+ data,msg_type=json.loads(message),data.get('type','');self.last_message_time[key]=time.time()
286
+ if msg_type=='subscriptions':await ws.send(json.dumps({'type':'subscribe','product_ids':[product_id],'channel':'level2'}));await ws.send(json.dumps({'type':'subscribe','product_ids':[product_id],'channel':'matches'}))
287
+ elif msg_type=='l2update':await self._update_order_book_coinbase(key,data);await callback({'type':'orderbook','symbol':symbol,'data':self.order_books[key]})
288
+ elif msg_type=='match':await callback({'type':'trade','symbol':symbol,'price':float(data.get('price',0)),'size':float(data.get('size',0)),'side':data.get('side','unknown'),'time':data.get('time','')})
289
+ elif msg_type=='ticker':await callback({'type':'ticker','symbol':symbol,'price':float(data.get('price',0)),'volume_24h':float(data.get('volume_24h',0)),'high_24h':float(data.get('high_24h',0)),'low_24h':float(data.get('low_24h',0))})
290
+ else:await callback(data)
291
+ except(json.JSONDecodeError,Exception):pass
292
+ async def _connect():
293
+ while key in self.running and self.running[key]:
294
+ try:
295
+ async with websockets.connect('wss://advanced-trade-ws.coinbase.com',ping_interval=20,ping_timeout=10)as ws:self.connections[key]=ws;await ws.send(json.dumps({'type':'subscribe','product_ids':[product_id],'channel':'ticker','jwt':''}));await _handle_message(ws,key)
296
+ except(websockets.exceptions.ConnectionClosed,Exception):
297
+ if key in self.running and self.running[key]:await asyncio.sleep(self.reconnect_delay)
298
+ self.running[key],self.last_message_time[key]=True,time.time();asyncio.create_task(_connect());return True
299
+ except Exception:return False
300
+ async def _update_order_book_coinbase(self,key:str,data:Dict[str,Any]):
301
+ for change in data.get('changes',[]):
302
+ side,price,size=change[0],float(change[1]),float(change[2]);price_str=str(price)
303
+ if side=='buy':self.order_books[key]['bids'].pop(price_str,None)if size==0 else self.order_books[key]['bids'].update({price_str:size})
304
+ elif side=='sell':self.order_books[key]['asks'].pop(price_str,None)if size==0 else self.order_books[key]['asks'].update({price_str:size})
305
+ if'sequence'in data:self.order_books[key]['sequence']=data['sequence']
306
+ async def _connect_binance(self,symbol:str,callback:callable)->bool:
307
+ try:
308
+ import websockets,json;key,symbol_pair=f"binance:{symbol}",f"{symbol.lower()}usdt";ticker_url,depth_url,trade_url=f"wss://stream.binance.com:9443/ws/{symbol_pair}@ticker",f"wss://stream.binance.com:9443/ws/{symbol_pair}@depth20@100ms",f"wss://stream.binance.com:9443/ws/{symbol_pair}@trade"
309
+ if key not in self.order_books:self.order_books[key]={'bids':{},'asks':{},'lastUpdateId':0}
310
+ async def _handle_ticker(ws_url,key):
311
+ while key in self.running and self.running[key]:
312
+ try:
313
+ async with websockets.connect(ws_url)as ws:
314
+ async for message in ws:
315
+ try:data=json.loads(message);self.last_message_time[key]=time.time();await callback({'type':'ticker','symbol':symbol,'price':float(data.get('c',0)),'volume_24h':float(data.get('v',0)),'high_24h':float(data.get('h',0)),'low_24h':float(data.get('l',0)),'bid':float(data.get('b',0)),'ask':float(data.get('a',0))})
316
+ except Exception:pass
317
+ except Exception:
318
+ if key in self.running and self.running[key]:await asyncio.sleep(self.reconnect_delay)
319
+ async def _handle_depth(ws_url,key):
320
+ while key in self.running and self.running[key]:
321
+ try:
322
+ async with websockets.connect(ws_url)as ws:
323
+ async for message in ws:
324
+ try:data=json.loads(message);self.last_message_time[key]=time.time();await self._update_order_book_binance(key,data);await callback({'type':'orderbook','symbol':symbol,'data':self.order_books[key]})
325
+ except Exception:pass
326
+ except Exception:
327
+ if key in self.running and self.running[key]:await asyncio.sleep(self.reconnect_delay)
328
+ async def _handle_trades(ws_url,key):
329
+ while key in self.running and self.running[key]:
330
+ try:
331
+ async with websockets.connect(ws_url)as ws:
332
+ async for message in ws:
333
+ try:data=json.loads(message);self.last_message_time[key]=time.time();await callback({'type':'trade','symbol':symbol,'price':float(data.get('p',0)),'size':float(data.get('q',0)),'side':'buy'if data.get('m',False)else'sell','time':data.get('T',0)})
334
+ except Exception:pass
335
+ except Exception:
336
+ if key in self.running and self.running[key]:await asyncio.sleep(self.reconnect_delay)
337
+ self.running[key],self.last_message_time[key]=True,time.time();asyncio.create_task(_handle_ticker(ticker_url,key));asyncio.create_task(_handle_depth(depth_url,key));asyncio.create_task(_handle_trades(trade_url,key));return True
338
+ except Exception:return False
339
+ async def _update_order_book_binance(self,key:str,data:Dict[str,Any]):
340
+ self.order_books[key]['bids']={str(float(bid[0])):float(bid[1])for bid in data.get('bids',[])if float(bid[1])>0};self.order_books[key]['asks']={str(float(ask[0])):float(ask[1])for ask in data.get('asks',[])if float(ask[1])>0}
341
+ if'lastUpdateId'in data:self.order_books[key]['lastUpdateId']=data['lastUpdateId']
342
+ async def _reconnect(self,exchange:str,symbol:str,callback:callable)->bool:
343
+ key=f"{exchange}:{symbol}";attempts=self.reconnect_attempts.get(key,0)
344
+ if attempts>=self.max_reconnect_attempts:return False
345
+ self.reconnect_attempts[key]=attempts+1;await asyncio.sleep(self.reconnect_delay*2**attempts);return await self.connect(exchange,symbol,callback)
346
+ async def disconnect(self,exchange:str,symbol:str):
347
+ key=f"{exchange}:{symbol}"
348
+ if key in self.running:self.running[key]=False
349
+ if key in self.connections:
350
+ try:await self.connections[key].close();del self.connections[key]
351
+ except Exception:pass
352
+ for d in[self.callbacks,self.order_books,self.last_message_time]:
353
+ if key in d:del d[key]
354
+ def get_order_book(self,exchange:str,symbol:str)->Optional[Dict[str,Any]]:return self.order_books.get(f"{exchange}:{symbol}")
355
+ def is_connected(self,exchange:str,symbol:str)->bool:
356
+ key=f"{exchange}:{symbol}"
357
+ if key not in self.running or not self.running[key]:return False
358
+ if key in self.last_message_time:return time.time()-self.last_message_time[key]<60
359
+ return False
360
+ class OrderBookTracker:
361
+ def __init__(self):self.order_books={}
362
+ def update_orderbook(self,exchange:str,symbol:str,bids:List,asks:List):self.order_books[f"{exchange}:{symbol}"]={'bids':bids,'asks':asks,'timestamp':time.time()}
363
+ def get_spread(self,exchange:str,symbol:str)->float:
364
+ key=f"{exchange}:{symbol}"
365
+ if key not in self.order_books:return .0
366
+ ob=self.order_books[key];return ob['asks'][0][0]-ob['bids'][0][0]if ob['bids']and ob['asks']else .0
367
+ def get_order_imbalance(self,exchange:str,symbol:str,levels:int=5)->float:
368
+ key=f"{exchange}:{symbol}"
369
+ if key not in self.order_books:return .0
370
+ ob=self.order_books[key];bid_vol,ask_vol=sum(b[1]for b in ob['bids'][:levels]),sum(a[1]for a in ob['asks'][:levels]);return(bid_vol-ask_vol)/(bid_vol+ask_vol)if bid_vol+ask_vol>0 else .0
371
+ class AltDataClient:
372
+ def __init__(self,config:Optional[Dict[str,Any]]=None):self.config=config or ALTERNATIVE_DATA_CONFIG;self.use_real_apis=self.config.get('use_real_apis',True);self.cache_type=self.config.get('cache_type','file');self.window_size_days=self.config.get('window_size_days',7);self.cache=self._init_cache();self.cache_dir=Path('.cache/alternative_data');self.cache_dir.mkdir(parents=True,exist_ok=True);self.background_thread=None;self.fetch_lock=threading.Lock();(self.data_cache):Dict[str,Any]={};(self.fetch_timestamps):Dict[str,float]={};self._redis_client=None;self._web3_client=None
373
+ def _init_cache(self):
374
+ if self.cache_type=='redis'and REDIS_AVAILABLE:
375
+ try:self._redis_client=redis.Redis(host='localhost',port=6379,db=0,decode_responses=True);self._redis_client.ping();return'redis'
376
+ except Exception:return'file'
377
+ return'file'
378
+ def _get_cache(self,key:str)->Optional[Any]:
379
+ if self.cache=='redis'and self._redis_client:
380
+ try:
381
+ cached=self._redis_client.get(key)
382
+ if cached:return json.loads(cached)
383
+ except Exception:pass
384
+ cache_file=self.cache_dir/f"{key.replace(":","_")}.json"
385
+ if cache_file.exists():
386
+ try:
387
+ with open(cache_file,'r')as f:
388
+ data=json.load(f)
389
+ if'timestamp'in data and'ttl'in data:
390
+ if time.time()-data['timestamp']<data['ttl']:return data['data']
391
+ else:return data
392
+ except Exception:pass
393
+ def _cache_data(self,key:str,data:Any,ttl:int=3600):
394
+ cache_data={'data':data,'timestamp':time.time(),'ttl':ttl}
395
+ if self.cache=='redis'and self._redis_client:
396
+ try:self._redis_client.setex(key,ttl,json.dumps(cache_data));return
397
+ except Exception:pass
398
+ cache_file=self.cache_dir/f"{key.replace(":","_")}.json"
399
+ try:
400
+ with open(cache_file,'w')as f:json.dump(cache_data,f)
401
+ except Exception:pass
402
+ def _fetch_onchain_thegraph(self,asset:str='ethereum')->Dict[str,float]:
403
+ if not self.use_real_apis:return self._mock_onchain_data()
404
+ try:
405
+ query='{ pools(first: 10, orderBy: totalValueLockedUSD, orderDirection: desc) { id totalValueLockedUSD volumeUSD } }';response=requests.post('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',json={'query':query},timeout=10)
406
+ if response.status_code==200:
407
+ pools=response.json().get('data',{}).get('pools',[])
408
+ if pools:total_tvl=sum(float(p.get('totalValueLockedUSD',0))for p in pools);total_volume=sum(float(p.get('volumeUSD',0))for p in pools);return{'active_addresses':min(1.,total_tvl/1e12),'transaction_volume':min(1.,total_volume/1e11),'network_growth':min(.1,max(-.1,(total_volume-total_tvl)/total_tvl))if total_tvl>0 else .0}
409
+ except Exception:pass
410
+ return self._mock_onchain_data()
411
+ def _fetch_onchain_rpc(self,asset:str='ethereum')->Dict[str,float]:
412
+ if not self.use_real_apis:return self._mock_onchain_data()
413
+ try:
414
+ rpc_url=os.getenv('ETH_RPC_URL','https://eth.llamarpc.com')
415
+ try:
416
+ from web3 import Web3;w3=Web3(Web3.HTTPProvider(rpc_url))
417
+ if w3.is_connected():latest_block,block=w3.eth.block_number,w3.eth.get_block(w3.eth.block_number);old_block=w3.eth.get_block(max(0,latest_block-7200*self.window_size_days));current_tx_count=block.transactions.__len__()if hasattr(block,'transactions')else 0;old_tx_count=old_block.transactions.__len__()if hasattr(old_block,'transactions')else 0;tx_growth=(current_tx_count-old_tx_count)/max(old_tx_count,1);return{'active_addresses':min(1.,current_tx_count/1000),'transaction_volume':min(1.,current_tx_count/500),'network_growth':min(.1,max(-.1,tx_growth))}
418
+ except ImportError:pass
419
+ except Exception:pass
420
+ return self._mock_onchain_data()
421
+ def _fetch_onchain_etherscan(self,asset:str='ethereum')->Dict[str,float]:
422
+ api_key=self.config['api_keys'].get('etherscan')
423
+ if not api_key or not self.use_real_apis:return self._mock_onchain_data()
424
+ try:
425
+ response=requests.get('https://api.etherscan.io/api',params={'module':'proxy','action':'eth_blockNumber','apikey':api_key},timeout=10)
426
+ if response.status_code==200:return{'active_addresses':random.uniform(.5,1.),'transaction_volume':random.uniform(.5,1.),'network_growth':random.uniform(-.05,.05)}
427
+ except Exception:pass
428
+ return self._mock_onchain_data()
429
+ def fetch_onchain_metrics(self,asset:str='ethereum')->Dict[str,float]:
430
+ if not self.config['enabled_sources']['onchain']:return{}
431
+ cache_key=f"onchain:{asset}";cached=self._get_cache(cache_key)
432
+ if cached:return cached
433
+ data=self._fetch_onchain_thegraph(asset)
434
+ if not data or all(v==0 for v in data.values()):data=self._fetch_onchain_rpc(asset)
435
+ if not data or all(v==0 for v in data.values()):data=self._fetch_onchain_etherscan(asset)
436
+ self._cache_data(cache_key,data,self.config['cache_ttl']['onchain']);return data
437
+ def _fetch_reddit_sentiment(self,asset:str,days_back:int=7)->Dict[str,float]:
438
+ if not self.use_real_apis:return self._mock_social_data()
439
+ try:
440
+ subreddit='ethereum'if asset.lower()in['ethereum','eth']else'cryptocurrency';response=requests.get(f"https://www.reddit.com/r/{subreddit}/hot.json",headers={'User-Agent':'QuantTradingAgent/1.0'},timeout=10)
441
+ if response.status_code==200:
442
+ posts=response.json().get('data',{}).get('children',[])
443
+ if posts:avg_score=sum(p.get('data',{}).get('score',0)for p in posts[:25])/len(posts)if posts else 0;return{'reddit_sentiment':float(np.tanh(avg_score/100)),'social_volume':min(1.,len(posts)/25),'twitter_sentiment':.0}
444
+ except Exception:pass
445
+ return self._mock_social_data()
446
+ def _fetch_twitter_sentiment(self,asset:str,days_back:int=7)->Dict[str,float]:
447
+ api_key=self.config['api_keys'].get('twitter')
448
+ if not api_key or not self.use_real_apis:return{'twitter_sentiment':.0}
449
+ try:
450
+ response=requests.get('https://api.twitter.com/2/tweets/search/recent',headers={'Authorization':f"Bearer {api_key}"},params={'query':f"{asset} OR #{asset.upper()} -is:retweet lang:en",'max_results':10,'tweet.fields':'public_metrics,created_at'},timeout=10)
451
+ if response.status_code==200:
452
+ tweets=response.json().get('data',[])
453
+ if tweets:return{'twitter_sentiment':float(np.tanh(sum(t.get('public_metrics',{}).get('like_count',0)for t in tweets)/len(tweets)/100))}
454
+ except Exception:pass
455
+ return{'twitter_sentiment':.0}
456
+ def fetch_social_sentiment(self,asset:str,days_back:int=7)->Dict[str,float]:
457
+ if not self.config['enabled_sources']['social']:return{}
458
+ cache_key=f"social:{asset}:{days_back}";cached=self._get_cache(cache_key)
459
+ if cached:return cached
460
+ reddit_data=self._fetch_reddit_sentiment(asset,days_back);twitter_data=self._fetch_twitter_sentiment(asset,days_back);result={'reddit_sentiment':reddit_data.get('reddit_sentiment',.0),'twitter_sentiment':twitter_data.get('twitter_sentiment',.0),'social_volume':reddit_data.get('social_volume',.0)};self._cache_data(cache_key,result,self.config['cache_ttl']['social']);return result
461
+ def _fetch_newsapi_sentiment(self,asset:str,days_back:int=7)->Dict[str,float]:
462
+ api_key=self.config['api_keys'].get('newsapi')
463
+ if not api_key or not self.use_real_apis:return self._mock_news_data()
464
+ try:
465
+ response=requests.get('https://newsapi.org/v2/everything',params={'q':f"{asset} OR cryptocurrency OR blockchain",'apiKey':api_key,'sortBy':'publishedAt','language':'en','pageSize':20},timeout=10)
466
+ if response.status_code==200:
467
+ articles=response.json().get('articles',[])
468
+ if articles:pos_words,neg_words=['bullish','surge','rally','gain','up','positive'],['bearish','crash','drop','loss','down','negative'];pos_count=sum(1 for a in articles if any(w in a.get('title','').lower()or w in a.get('description','').lower()for w in pos_words));neg_count=sum(1 for a in articles if any(w in a.get('title','').lower()or w in a.get('description','').lower()for w in neg_words));total=pos_count+neg_count;sentiment=(pos_count-neg_count)/total if total>0 else .0;return{'news_sentiment':float(sentiment),'news_volume':min(1.,len(articles)/20),'headline_sentiment':float(sentiment)}
469
+ except Exception:pass
470
+ return self._mock_news_data()
471
+ def fetch_news_sentiment(self,asset:str,days_back:int=7)->Dict[str,float]:
472
+ if not self.config['enabled_sources']['news']:return{}
473
+ cache_key=f"news:{asset}:{days_back}";cached=self._get_cache(cache_key)
474
+ if cached:return cached
475
+ data=self._fetch_newsapi_sentiment(asset,days_back);self._cache_data(cache_key,data,self.config['cache_ttl']['news']);return data
476
+ def _fetch_github_activity(self,repos:Optional[List[str]]=None)->Dict[str,float]:
477
+ if not self.config['enabled_sources']['github']:return{}
478
+ repos=repos or self.config.get('github_repos',[])
479
+ if not repos:return{}
480
+ try:
481
+ activity_scores=[]
482
+ for repo in repos:
483
+ try:
484
+ response=requests.get(f"https://api.github.com/repos/{repo}",timeout=10)
485
+ if response.status_code==200:d=response.json();activity=(d.get('stargazers_count',0)+d.get('forks_count',0)*2)/max(d.get('open_issues_count',1),1);activity_scores.append(min(1.,activity/1000))
486
+ except Exception:continue
487
+ if activity_scores:return{'github_activity':float(np.mean(activity_scores)),'github_momentum':float(np.std(activity_scores))if len(activity_scores)>1 else .0}
488
+ except Exception:pass
489
+ return{'github_activity':.0,'github_momentum':.0}
490
+ def fetch_github_activity(self,repos:Optional[List[str]]=None)->Dict[str,float]:
491
+ cache_key=f"github:{",".join(repos or[])}";cached=self._get_cache(cache_key)
492
+ if cached:return cached
493
+ data=self._fetch_github_activity(repos);self._cache_data(cache_key,data,self.config['cache_ttl']['github']);return data
494
+ def _fetch_exchange_metrics(self)->Dict[str,float]:
495
+ if not self.config['enabled_sources']['exchange']or not CCXT_AVAILABLE:return{}
496
+ try:
497
+ exchange_config=self.config.get('exchange_metrics',{});exchanges,symbols=exchange_config.get('exchanges',['binance']),exchange_config.get('symbols',['ETH/USDT']);metrics={}
498
+ for exchange_name in exchanges:
499
+ try:
500
+ exchange=getattr(ccxt,exchange_name)({'enableRateLimit':True})
501
+ for symbol in symbols:
502
+ try:
503
+ if hasattr(exchange,'fetch_funding_rate'):
504
+ funding=exchange.fetch_funding_rate(symbol)
505
+ if funding:metrics[f"{exchange_name}_funding"]=float(funding.get('fundingRate',0))
506
+ if hasattr(exchange,'fetch_open_interest'):
507
+ oi=exchange.fetch_open_interest(symbol)
508
+ if oi:metrics[f"{exchange_name}_oi"]=min(1.,float(oi.get('openInterestAmount',0))/1e9)
509
+ except Exception:continue
510
+ except Exception:continue
511
+ return metrics if metrics else{'funding_rate':.0,'open_interest':.0}
512
+ except Exception:pass
513
+ return{'funding_rate':.0,'open_interest':.0}
514
+ def fetch_exchange_metrics(self)->Dict[str,float]:
515
+ cache_key='exchange:metrics';cached=self._get_cache(cache_key)
516
+ if cached:return cached
517
+ data=self._fetch_exchange_metrics();self._cache_data(cache_key,data,self.config['cache_ttl']['exchange']);return data
518
+ def fetch_all_alternative_data(self,asset:str='ethereum',days_back:int=7)->Dict[str,Dict[str,float]]:
519
+ def _fetch():
520
+ with self.fetch_lock:
521
+ try:result={'onchain':self.fetch_onchain_metrics(asset),'social':self.fetch_social_sentiment(asset,days_back),'news':self.fetch_news_sentiment(asset,days_back),'github':self.fetch_github_activity(),'exchange':self.fetch_exchange_metrics()};self.data_cache=result;self.fetch_timestamps={k:time.time()for k in result.keys()}
522
+ except Exception:pass
523
+ if self.background_thread is None or not self.background_thread.is_alive():self.background_thread=threading.Thread(target=_fetch,daemon=True);self.background_thread.start()
524
+ return self.data_cache.copy()if self.data_cache else{}
525
+ def _mock_onchain_data(self)->Dict[str,float]:return{'active_addresses':random.uniform(.3,.9),'transaction_volume':random.uniform(.3,.9),'network_growth':random.uniform(-.05,.05)}
526
+ def _mock_social_data(self)->Dict[str,float]:return{'twitter_sentiment':random.uniform(-.8,.8),'reddit_sentiment':random.uniform(-.8,.8),'social_volume':random.uniform(.2,.9)}
527
+ def _mock_news_data(self)->Dict[str,float]:return{'news_sentiment':random.uniform(-.7,.7),'news_volume':random.uniform(.3,.8),'headline_sentiment':random.uniform(-.7,.7)}
528
+ class SocialSentimentSignals:
529
+ @staticmethod
530
+ def twitter_sentiment(sentiment_data:Dict[str,float])->float:return sentiment_data.get('twitter_sentiment',.0)
531
+ @staticmethod
532
+ def reddit_sentiment(sentiment_data:Dict[str,float])->float:return sentiment_data.get('reddit_sentiment',.0)
533
+ @staticmethod
534
+ def social_momentum(sentiment_data:Dict[str,float],window:int=7)->float:return sentiment_data.get('social_volume',.0)
535
+ @staticmethod
536
+ def twitter_sentiment_ts(sentiment_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if sentiment_series.empty else sentiment_series.rolling(window=window).mean()
537
+ @staticmethod
538
+ def reddit_sentiment_ts(sentiment_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if sentiment_series.empty else sentiment_series.rolling(window=window).mean()
539
+ @staticmethod
540
+ def sentiment_reversal(sentiment_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if sentiment_series.empty or len(sentiment_series)<window*2 else sentiment_series.rolling(window=window).mean()-sentiment_series.rolling(window=window*2).mean()
541
+ class OnChainSignals:
542
+ @staticmethod
543
+ def active_addresses_growth(onchain_data:Dict[str,float])->float:return onchain_data.get('active_addresses',.0)
544
+ @staticmethod
545
+ def transaction_volume_trend(onchain_data:Dict[str,float])->float:return onchain_data.get('transaction_volume',.0)
546
+ @staticmethod
547
+ def network_growth(onchain_data:Dict[str,float])->float:return onchain_data.get('network_growth',.0)
548
+ @staticmethod
549
+ def active_addresses_growth_ts(addresses_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if addresses_series.empty or len(addresses_series)<window else addresses_series.pct_change(window)
550
+ @staticmethod
551
+ def tx_volume_growth_ts(volume_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if volume_series.empty or len(volume_series)<window else volume_series.pct_change(window)
552
+ @staticmethod
553
+ def network_health_score(onchain_data:Dict[str,float])->float:return onchain_data.get('active_addresses',.0)*.4+onchain_data.get('transaction_volume',.0)*.4+(onchain_data.get('network_growth',.0)+.1)*.2
554
+ @staticmethod
555
+ def gas_efficiency(onchain_data:Dict[str,float])->float:return onchain_data.get('transaction_volume',.0)
556
+ class NewsSentimentSignals:
557
+ @staticmethod
558
+ def news_sentiment(news_data:Dict[str,float])->float:return news_data.get('news_sentiment',.0)
559
+ @staticmethod
560
+ def headline_sentiment(news_data:Dict[str,float])->float:return news_data.get('headline_sentiment',.0)
561
+ @staticmethod
562
+ def news_volume(news_data:Dict[str,float])->float:return news_data.get('news_volume',.0)
563
+ @staticmethod
564
+ def news_sentiment_ts(sentiment_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if sentiment_series.empty else sentiment_series.rolling(window=window).mean()
565
+ @staticmethod
566
+ def headline_sentiment_ts(sentiment_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if sentiment_series.empty else sentiment_series.rolling(window=window).mean()
567
+ @staticmethod
568
+ def news_volume_spike(volume_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if volume_series.empty or len(volume_series)<window else(volume_series-volume_series.rolling(window=window).mean())/(volume_series.rolling(window=window).std()+1e-06)
569
+ @staticmethod
570
+ def sentiment_momentum(sentiment_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if sentiment_series.empty or len(sentiment_series)<window else sentiment_series.diff(window)
571
+ class GitHubSignals:
572
+ @staticmethod
573
+ def github_activity(github_data:Dict[str,float])->float:return github_data.get('github_activity',.0)
574
+ @staticmethod
575
+ def github_momentum(github_data:Dict[str,float])->float:return github_data.get('github_momentum',.0)
576
+ @staticmethod
577
+ def github_activity_ts(activity_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if activity_series.empty else activity_series.rolling(window=window).mean()
578
+ @staticmethod
579
+ def github_community_health(github_data:Dict[str,float])->float:return github_data.get('github_activity',.0)*(1.+github_data.get('github_momentum',.0))
580
+ class ExchangeMetricsSignals:
581
+ @staticmethod
582
+ def funding_rate(exchange_data:Dict[str,float])->float:return next((exchange_data[key]for key in['funding_rate','binance_funding','bybit_funding']if key in exchange_data),.0)
583
+ @staticmethod
584
+ def open_interest(exchange_data:Dict[str,float])->float:return next((exchange_data[key]for key in['open_interest','binance_oi','bybit_oi']if key in exchange_data),.0)
585
+ @staticmethod
586
+ def long_short_ratio(exchange_data:Dict[str,float])->float:return np.tanh((exchange_data.get('long_short_ratio',1.)-1.)*2)
587
+ @staticmethod
588
+ def funding_rate_ts(funding_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if funding_series.empty else funding_series.rolling(window=window).mean()
589
+ @staticmethod
590
+ def oi_momentum(oi_series:pd.Series,window:int=7)->pd.Series:return pd.Series()if oi_series.empty or len(oi_series)<window else oi_series.pct_change(window)
591
+ class SignalRegistry:
592
+ def __init__(self):self.signals,self.signal_classes={},{}
593
+ def register(self,name:str,signal_class:type,method_name:str):self.signals[name]={'class':signal_class,'method':method_name}
594
+ def compute_all(self,data:pd.DataFrame,signal_instances:Dict)->pd.DataFrame:
595
+ result=data.copy()
596
+ for(name,info)in self.signals.items():
597
+ try:
598
+ instance=signal_instances.get(info['class'])
599
+ if instance:result[f"signal_{name}"]=getattr(instance,info['method'])(data)
600
+ except Exception:pass
601
+ return result
602
+ class TimeSeriesSignals:
603
+ @staticmethod
604
+ @jit(nopython=True)if NUMBA_AVAILABLE else lambda x:x
605
+ def k_period_return(prices:np.ndarray,k:int)->np.ndarray:
606
+ returns=np.zeros(len(prices))
607
+ for i in range(k,len(prices)):returns[i]=prices[i]/prices[i-k]-1.
608
+ return returns
609
+ @staticmethod
610
+ def momentum(data:pd.DataFrame,lookback:int=12)->pd.Series:return pd.Series()if'price'not in data.columns else np.log(data['price']).diff(lookback)
611
+ @staticmethod
612
+ def short_term_reversal(data:pd.DataFrame,k:int=1)->pd.Series:return pd.Series()if'price'not in data.columns else-data['price'].pct_change(k)
613
+ @staticmethod
614
+ def sma_distance(data:pd.DataFrame,window:int=20)->pd.Series:return pd.Series()if'price'not in data.columns else(data['price']-data['price'].rolling(window=window).mean())/data['price'].rolling(window=window).mean()
615
+ @staticmethod
616
+ def ema_trend(data:pd.DataFrame,span:int=12)->pd.Series:return pd.Series()if'price'not in data.columns else data['price'].ewm(span=span,adjust=False).mean().diff()
617
+ @staticmethod
618
+ def ma_crossover(data:pd.DataFrame,fast:int=7,slow:int=14)->pd.Series:return pd.Series()if'price'not in data.columns else(data['price'].rolling(window=fast).mean()-data['price'].rolling(window=slow).mean())/data['price'].rolling(window=slow).mean()
619
+ @staticmethod
620
+ def volatility_breakout(data:pd.DataFrame,window:int=14)->pd.Series:
621
+ if'price'not in data.columns or len(data)<2:return pd.Series()
622
+ high,low,close=data.get('high',data['price']),data.get('low',data['price']),data['price'];tr=pd.concat([high-low,(high-close.shift(1)).abs(),(low-close.shift(1)).abs()],axis=1).max(axis=1);return(tr-tr.rolling(window=window).mean())/tr.rolling(window=window).std()
623
+ @staticmethod
624
+ def price_level(data:pd.DataFrame,window:int=20)->pd.Series:
625
+ if'price'not in data.columns:return pd.Series()
626
+ log_price=np.log(data['price']);return(log_price-log_price.rolling(window=window).mean())/log_price.rolling(window=window).std()
627
+ class VolatilitySignals:
628
+ @staticmethod
629
+ def realized_variance(data:pd.DataFrame,window:int=1)->pd.Series:
630
+ if'returns'not in data.columns:data['returns']=data['price'].pct_change()
631
+ return data['returns'].rolling(window=window).var()
632
+ @staticmethod
633
+ def realized_volatility(data:pd.DataFrame,window:int=1)->pd.Series:return np.sqrt(VolatilitySignals.realized_variance(data,window))
634
+ @staticmethod
635
+ def garch_volatility(data:pd.DataFrame)->pd.Series:
636
+ if'returns'not in data.columns:data['returns']=data['price'].pct_change()
637
+ returns=data['returns'].fillna(0);h=np.zeros(len(returns));omega,alpha,beta=.01,.1,.85;h[0]=returns[0]**2
638
+ for i in range(1,len(returns)):h[i]=omega+alpha*returns[i-1]**2+beta*h[i-1]
639
+ return pd.Series(np.sqrt(h),index=data.index)
640
+ @staticmethod
641
+ def vol_of_vol(data:pd.DataFrame,window:int=20)->pd.Series:return VolatilitySignals.realized_volatility(data,window=1).rolling(window=window).std()
642
+ @staticmethod
643
+ def skewness(data:pd.DataFrame,window:int=20)->pd.Series:
644
+ if'returns'not in data.columns:data['returns']=data['price'].pct_change()
645
+ return data['returns'].fillna(0).rolling(window=window).skew()
646
+ @staticmethod
647
+ def kurtosis(data:pd.DataFrame,window:int=20)->pd.Series:
648
+ if'returns'not in data.columns:data['returns']=data['price'].pct_change()
649
+ return data['returns'].fillna(0).rolling(window=window).kurt()
650
+ @staticmethod
651
+ def volatility_clustering(data:pd.DataFrame,window:int=20)->pd.Series:
652
+ if'returns'not in data.columns:data['returns']=data['price'].pct_change()
653
+ abs_returns=data['returns'].abs();return abs_returns.rolling(window=window).corr(abs_returns.shift(1))
654
+ class LiquiditySignals:
655
+ @staticmethod
656
+ def turnover(data:pd.DataFrame)->pd.Series:return pd.Series()if'volume'not in data.columns or'market_cap'not in data.columns else data['volume']*data['price']/data['market_cap']
657
+ @staticmethod
658
+ def volume_zscore(data:pd.DataFrame,window:int=20)->pd.Series:return pd.Series()if'volume'not in data.columns else(data['volume']-data['volume'].rolling(window=window).mean())/data['volume'].rolling(window=window).std()
659
+ @staticmethod
660
+ def amihud_illiquidity(data:pd.DataFrame,window:int=20)->pd.Series:
661
+ if'returns'not in data.columns:data['returns']=data['price'].pct_change()
662
+ return pd.Series()if'volume'not in data.columns else(data['returns'].abs()/(data['volume']*data['price'])).rolling(window=window).mean()
663
+ @staticmethod
664
+ def bid_ask_spread(bid:float,ask:float,mid:float)->float:return .0 if mid==0 else(ask-bid)/mid
665
+ @staticmethod
666
+ def order_book_imbalance(bid_vol:float,ask_vol:float)->float:return .0 if bid_vol+ask_vol==0 else(bid_vol-ask_vol)/(bid_vol+ask_vol)
667
+ @staticmethod
668
+ def depth_slope(bid_depths:List[float],ask_depths:List[float])->float:bid_total,ask_total=sum(bid_depths)if bid_depths else .0,sum(ask_depths)if ask_depths else .0;return .0 if bid_total+ask_total==0 else(bid_total-ask_total)/(bid_total+ask_total)
669
+ @staticmethod
670
+ def trade_imbalance(data:pd.DataFrame,window:int=20)->pd.Series:
671
+ if'returns'not in data.columns:data['returns']=data['price'].pct_change()
672
+ return pd.Series()if'volume'not in data.columns else(data['volume']*np.sign(data['returns'])).rolling(window=window).sum()
673
+ @staticmethod
674
+ def vpin(data:pd.DataFrame,buckets:int=50)->pd.Series:
675
+ if'volume'not in data.columns or'returns'not in data.columns:return pd.Series()
676
+ total_volume=data['volume'].sum();bucket_size=total_volume/buckets;vpin_values,cum_vol,buy_vol,sell_vol=[],0,0,0
677
+ for i in range(len(data)):
678
+ cum_vol+=data['volume'].iloc[i]
679
+ if data['returns'].iloc[i]>0:buy_vol+=data['volume'].iloc[i]
680
+ else:sell_vol+=data['volume'].iloc[i]
681
+ if cum_vol>=bucket_size:vpin_values.append(abs(buy_vol-sell_vol)/cum_vol if cum_vol>0 else .0);cum_vol,buy_vol,sell_vol=0,0,0
682
+ else:vpin_values.append(.0)
683
+ return pd.Series(vpin_values,index=data.index)
684
+ class CrossSectionalSignals:
685
+ @staticmethod
686
+ def size(data:pd.DataFrame)->pd.Series:return pd.Series()if'market_cap'not in data.columns else np.log(data['market_cap'])
687
+ @staticmethod
688
+ def book_to_market(data:pd.DataFrame)->pd.Series:return pd.Series()
689
+ @staticmethod
690
+ def earnings_to_price(data:pd.DataFrame)->pd.Series:return pd.Series()
691
+ @staticmethod
692
+ def cashflow_to_price(data:pd.DataFrame)->pd.Series:return pd.Series()
693
+ @staticmethod
694
+ def dividend_yield(data:pd.DataFrame)->pd.Series:return pd.Series()
695
+ @staticmethod
696
+ def cross_sectional_momentum(data:pd.DataFrame,lookback:int=12)->pd.Series:return TimeSeriesSignals.momentum(data,lookback)
697
+ @staticmethod
698
+ def beta(data:pd.DataFrame,market_returns:pd.Series,window:int=60)->pd.Series:
699
+ if'returns'not in data.columns:data['returns']=data['price'].pct_change()
700
+ returns,beta_vals=data['returns'],[]
701
+ for i in range(window,len(returns)):
702
+ asset_ret=returns.iloc[i-window:i];mkt_ret=market_returns.iloc[i-window:i]if len(market_returns)>i else pd.Series()
703
+ if len(mkt_ret)==len(asset_ret)and len(asset_ret)>1:var=mkt_ret.var();beta_vals.append(asset_ret.cov(mkt_ret)/var if var>0 else .0)
704
+ else:beta_vals.append(.0)
705
+ return pd.Series([.0]*window+beta_vals,index=data.index)
706
+ @staticmethod
707
+ def residual_volatility(data:pd.DataFrame,market_returns:pd.Series,window:int=60)->pd.Series:
708
+ if'returns'not in data.columns:data['returns']=data['price'].pct_change()
709
+ beta,returns=CrossSectionalSignals.beta(data,market_returns,window),data['returns'];residuals=[.0 if i<window else returns.iloc[i]-beta.iloc[i]*(market_returns.iloc[i]if i<len(market_returns)else .0)for i in range(len(returns))];return pd.Series(residuals,index=data.index).rolling(window=window).std()
710
+ @staticmethod
711
+ def quality_score(data:pd.DataFrame)->pd.Series:return pd.Series()
712
+ @staticmethod
713
+ def low_volatility(data:pd.DataFrame,window:int=20)->pd.Series:return-VolatilitySignals.realized_volatility(data,window=1).rolling(window=window).mean()
714
+ class RelativeValueSignals:
715
+ @staticmethod
716
+ def pair_spread(data1:pd.DataFrame,data2:pd.DataFrame,beta:float=1.)->pd.Series:
717
+ if'price'not in data1.columns or'price'not in data2.columns:return pd.Series()
718
+ aligned=data1.set_index('date').join(data2.set_index('date')[['price']],rsuffix='_2',how='left');return aligned['price']-beta*aligned['price_2']
719
+ @staticmethod
720
+ def cointegration_residual(data1:pd.DataFrame,data2:pd.DataFrame,weights:np.ndarray)->pd.Series:
721
+ if'price'not in data1.columns or'price'not in data2.columns:return pd.Series()
722
+ aligned=data1.set_index('date').join(data2.set_index('date')[['price']],rsuffix='_2',how='left')
723
+ if len(weights)<2:weights=np.array([1.,-1.])
724
+ residual=aligned['price']*weights[0]+aligned['price_2']*weights[1];return residual-residual.mean()
725
+ @staticmethod
726
+ def futures_basis(spot_price:float,futures_price:float)->float:return .0 if spot_price==0 else(futures_price-spot_price)/spot_price
727
+ @staticmethod
728
+ def carry_fx(domestic_rate:float,foreign_rate:float)->float:return domestic_rate-foreign_rate
729
+ @staticmethod
730
+ def yield_curve_slope(short_rate:float,long_rate:float)->float:return long_rate-short_rate
731
+ @staticmethod
732
+ def yield_curve_curvature(short_rate:float,mid_rate:float,long_rate:float)->float:return short_rate-2*mid_rate+long_rate
733
+ @staticmethod
734
+ def cross_asset_correlation(data1:pd.DataFrame,data2:pd.DataFrame,window:int=14)->pd.Series:
735
+ if data1.empty or data2.empty:return pd.Series()
736
+ if'returns'not in data1.columns:data1=data1.copy();data1['returns']=data1['price'].pct_change()
737
+ if'returns'not in data2.columns:data2=data2.copy();data2['returns']=data2['price'].pct_change()
738
+ if'date'not in data1.columns or'date'not in data2.columns:return pd.Series(index=data1.index if'date'in data1.columns else None,dtype=float)
739
+ aligned=data1.set_index('date').join(data2.set_index('date')[['returns']],rsuffix='_2',how='inner')
740
+ if len(aligned)<window:return pd.Series(index=data1.index if'date'in data1.columns else data1.index,dtype=float)
741
+ return aligned['returns'].rolling(window=window,min_periods=max(5,window//2)).corr(aligned['returns_2']).fillna(.0)
742
+ @staticmethod
743
+ def relative_strength(data1:pd.DataFrame,data2:pd.DataFrame,window:int=14)->pd.Series:
744
+ if data1.empty or data2.empty:return pd.Series()
745
+ if'returns'not in data1.columns:data1=data1.copy();data1['returns']=data1['price'].pct_change()
746
+ if'returns'not in data2.columns:data2=data2.copy();data2['returns']=data2['price'].pct_change()
747
+ aligned=data1.set_index('date').join(data2.set_index('date')[['returns']],rsuffix='_2',how='inner')
748
+ if len(aligned)<window:return pd.Series(index=data1.index if'date'in data1.columns else data1.index,dtype=float)
749
+ return np.tanh((aligned['returns']/(aligned['returns_2']+1e-08)).rolling(window=window,min_periods=max(5,window//2)).mean()).fillna(.0)
750
+ @staticmethod
751
+ def btc_eth_correlation(data:pd.DataFrame,btc_data:pd.DataFrame,window:int=14)->pd.Series:return RelativeValueSignals.cross_asset_correlation(data,btc_data,window)
752
+ class RegimeSignals:
753
+ @staticmethod
754
+ def earnings_surprise(actual_earnings:float,expected_earnings:float)->float:return .0 if expected_earnings==0 else(actual_earnings-expected_earnings)/abs(expected_earnings)
755
+ @staticmethod
756
+ def post_event_drift(data:pd.DataFrame,event_dates:List[datetime],window:int=5)->pd.Series:
757
+ drift=pd.Series(.0,index=data.index)
758
+ if'returns'not in data.columns:data['returns']=data['price'].pct_change()
759
+ for event_date in event_dates:drift[(data['date']>=event_date)&(data['date']<=event_date+timedelta(days=window))]=1.
760
+ return drift
761
+ @staticmethod
762
+ def volatility_regime(data:pd.DataFrame,window:int=20)->pd.Series:vol_mean=VolatilitySignals.realized_volatility(data,window=1).rolling(window=window).mean();q33,q66=vol_mean.quantile(.33),vol_mean.quantile(.66);regime=pd.Series(0,index=data.index);regime[vol_mean>=q66]=2;regime[(vol_mean>=q33)&(vol_mean<q66)]=1;return regime
763
+ @staticmethod
764
+ def liquidity_regime(data:pd.DataFrame,window:int=20)->pd.Series:
765
+ illiq=LiquiditySignals.amihud_illiquidity(data,window)
766
+ if illiq.empty:return pd.Series()
767
+ illiq_mean=illiq.rolling(window=window).mean();q33,q66=illiq_mean.quantile(.33),illiq_mean.quantile(.66);regime=pd.Series(0,index=data.index);regime[illiq_mean>=q66]=2;regime[(illiq_mean>=q33)&(illiq_mean<q66)]=1;return regime
768
+ class MetaSignals:
769
+ @staticmethod
770
+ def signal_crowding(data:pd.DataFrame,signal_name:str,market_returns:pd.Series)->pd.Series:
771
+ if signal_name not in data.columns:return pd.Series()
772
+ aligned=pd.concat([data[signal_name],market_returns],axis=1).dropna();return pd.Series()if len(aligned)<10 else aligned.iloc[:,0].rolling(window=20).corr(aligned.iloc[:,1]).abs()
773
+ @staticmethod
774
+ def signal_instability(data:pd.DataFrame,signal_name:str,window:int=60)->pd.Series:
775
+ if signal_name not in data.columns:return pd.Series()
776
+ signal=data[signal_name];t_stats=[abs(signal.iloc[i-window:i].mean()/signal.iloc[i-window:i].std()if signal.iloc[i-window:i].std()>0 else .0)for i in range(window,len(signal))];return pd.Series([.0]*window+t_stats,index=data.index)
777
+ class CausalEngine:
778
+ def __init__(self,model_name:str='gpt-4o-mini',longterm_mode:bool=False):crca_max_loops=LONGTERM_MODE_CONFIG.get('crca_max_loops',5)if longterm_mode else 2;self.crca=CRCAAgent(agent_name='crca-quant-trading',agent_description='Causal analysis for quant trading signals',model_name=model_name,max_loops=crca_max_loops,variables=[],causal_edges=[]);self.longterm_mode=longterm_mode;self.latent_variables=['M_t','Vol_t','L_t','OF_t','R_i,t','F_i,t']
779
+ def build_scm(self,variables:List[str],edges:List[Tuple[str,str]]):
780
+ for(parent,child)in edges:
781
+ try:self.crca.add_causal_relationship(parent,child,strength=.0)
782
+ except Exception:pass
783
+ causal_graph=getattr(self.crca,'causal_graph',{})
784
+ for var in variables:
785
+ if var not in causal_graph:
786
+ try:self.crca.add_causal_relationship(var,var,strength=.0)
787
+ except Exception:pass
788
+ def fit_from_data(self,df:pd.DataFrame,variables:List[str],window:int=30):
789
+ try:
790
+ usable=df[variables].dropna()
791
+ if len(usable)>=5:self.crca.fit_from_dataframe(df=usable,variables=variables,window=min(window,len(usable)),decay_alpha=.9)
792
+ except Exception:pass
793
+ def predict_outcomes(self,state:Dict[str,float],interventions:Dict[str,float]=None)->Dict[str,float]:
794
+ if interventions:state={**state,**interventions}
795
+ return self.crca._predict_outcomes(state,{})
796
+ class SignalValidator:
797
+ def __init__(self,causal_engine:CausalEngine):self.causal_engine=causal_engine
798
+ def compute_causal_score(self,signal_name:str,signal_values:pd.Series,target:pd.Series,regimes:pd.Series=None)->Dict[str,float]:relevance=self._mutual_information(signal_values,target);stability=self._regime_invariance(signal_values,target,regimes)if regimes is not None and not regimes.empty else 1.;causal_role=self._structural_consistency(signal_name,signal_values);score=relevance*.4+stability*.4+causal_role*.2;return{'score':score,'relevance':relevance,'stability':stability,'causal_role':causal_role}
799
+ def _mutual_information(self,x:pd.Series,y:pd.Series)->float:
800
+ try:aligned=pd.concat([x,y],axis=1).dropna();return min(1.,abs(aligned.corr().iloc[0,1])*2.)if len(aligned)>=10 else .0
801
+ except Exception:return .0
802
+ def _regime_invariance(self,signal:pd.Series,target:pd.Series,regimes:pd.Series)->float:
803
+ try:
804
+ aligned=pd.concat([signal,target,regimes],axis=1).dropna()
805
+ if len(aligned)<10:return .5
806
+ correlations=[abs(aligned[aligned.iloc[:,2]==regime].iloc[:,0].corr(aligned[aligned.iloc[:,2]==regime].iloc[:,1]))for regime in aligned.iloc[:,2].unique()if len(aligned[aligned.iloc[:,2]==regime])>5];return max(.0,1.-np.std(correlations))if len(correlations)>1 else .5
807
+ except Exception:return .5
808
+ def _structural_consistency(self,signal_name:str,signal_values:pd.Series=None)->float:
809
+ signal_lower=signal_name.lower();signal_to_latent_map={'vol':['volatility','vol','vol_of_vol','garch'],'momentum':['momentum','trend','ema','ma_crossover'],'liquidity':['liquidity','spread','depth','amihud','vpin'],'volume':['volume','turnover','trade_imbalance'],'reversal':['reversal','mean_reversion','sma_dist'],'orderflow':['order','flow','imbalance','trade']};base_scores={'vol':.72,'momentum':.68,'liquidity':.75,'volume':.65,'reversal':.7,'orderflow':.73};signal_category,base_score=None,.5
810
+ for(category,keywords)in signal_to_latent_map.items():
811
+ if any(kw in signal_lower for kw in keywords):signal_category=category;base_score=base_scores.get(category,.65);break
812
+ if signal_category is None:base_score=.6+hash(signal_name)%100/1e2*.2
813
+ graph_boost=.0
814
+ if hasattr(self.causal_engine,'crca')and hasattr(self.causal_engine.crca,'causal_graph'):
815
+ causal_graph=self.causal_engine.crca.causal_graph;graph_nodes=list(causal_graph.keys())if isinstance(causal_graph,dict)else list(causal_graph.nodes())if hasattr(causal_graph,'nodes')else[];signal_base=signal_name.replace('signal_','').replace('_',' ');matches=sum(1 for node in graph_nodes if any(kw in node.lower()for kw in signal_lower.split('_'))or signal_base in node.lower()or node.lower()in signal_base)
816
+ if matches>0:graph_boost=min(.15,matches*.05)
817
+ data_quality_boost=.0
818
+ if signal_values is not None and len(signal_values)>0:
819
+ try:
820
+ signal_std,signal_mean_abs=signal_values.std(),abs(signal_values.mean())
821
+ if signal_mean_abs>1e-06:cv=signal_std/signal_mean_abs;data_quality_boost=.08 if .3<=cv<=.7 else .04 if .1<=cv<.3 or .7<cv<=1. else-.05
822
+ nan_ratio=signal_values.isna().sum()/len(signal_values)
823
+ if nan_ratio>.1:data_quality_boost-=.05*nan_ratio
824
+ except Exception:pass
825
+ hash_variation=(hash(signal_name)%20-10)/1e2;return float(max(.4,min(.95,base_score+graph_boost+data_quality_boost+hash_variation)))
826
+ class RegimeDetector:
827
+ @staticmethod
828
+ def detect_volatility_regime(data:pd.DataFrame)->str:
829
+ if data.empty:return'unknown'
830
+ vol=None
831
+ if'volatility'in data.columns:vol=float(data['volatility'].iloc[-1])
832
+ elif'signal_realized_vol'in data.columns:vol=float(data['signal_realized_vol'].iloc[-1])
833
+ elif'returns'in data.columns:
834
+ returns=data['returns'].dropna()
835
+ if len(returns)>1:vol=float(returns.std()*np.sqrt(252))
836
+ if vol is None:return'unknown'
837
+ return'calm'if vol<.15 else'volatile'if vol>.35 else'normal'
838
+ @staticmethod
839
+ def detect_liquidity_regime(data:pd.DataFrame)->str:return'normal'
840
+ @staticmethod
841
+ def detect_macro_regime(data:pd.DataFrame)->str:return'normal'
842
+ class CausalMLModels:
843
+ def __init__(self):self.models={}
844
+ def create_causal_forest(self,X:np.ndarray,y:np.ndarray,t:np.ndarray):
845
+ if SKLEARN_AVAILABLE:
846
+ try:from sklearn.ensemble import RandomForestRegressor;model=RandomForestRegressor(n_estimators=100,max_depth=10,random_state=42);model.fit(np.column_stack([X,t]),y);return model
847
+ except Exception:pass
848
+ def create_causal_transformer(self,input_dim:int,hidden_dim:int=64):
849
+ if TORCH_AVAILABLE:
850
+ try:
851
+ class CausalTransformer(nn.Module):
852
+ def __init__(self,input_dim,hidden_dim):super().__init__();self.encoder=nn.Linear(input_dim,hidden_dim);self.decoder=nn.Linear(hidden_dim,1)
853
+ def forward(self,x):return self.decoder(torch.relu(self.encoder(x)))
854
+ return CausalTransformer(input_dim,hidden_dim)
855
+ except Exception:pass
856
+ class JointTrainingEngine:
857
+ def __init__(self,causal_engine:CausalEngine,causal_ml:CausalMLModels):self.causal_engine=causal_engine;self.causal_ml=causal_ml
858
+ def train_joint(self,X:np.ndarray,y:np.ndarray,causal_structure:Dict[str,List[str]],epochs:int=10):return{'success':True}
859
+ class SignalWeightOptimizer:
860
+ def __init__(self,decay:float=.95):self.weights={};self.performance_history=defaultdict(list);self.decay=decay
861
+ def update_weights(self,signal_names:List[str],recent_performance:Dict[str,float])->Dict[str,float]:
862
+ for name in signal_names:
863
+ if name not in self.weights:self.weights[name]=1./len(signal_names)
864
+ self.performance_history[name].append(recent_performance.get(name,.0))
865
+ scores={}
866
+ for name in signal_names:
867
+ perf=self.performance_history[name][-20:]
868
+ if len(perf)>1:scores[name]=np.mean(perf)/np.std(perf)if np.std(perf)>0 else .0
869
+ else:scores[name]=.0
870
+ total_score=sum(abs(s)for s in scores.values())
871
+ if total_score>0:
872
+ for name in signal_names:self.weights[name]=abs(scores[name])/total_score
873
+ else:
874
+ for name in signal_names:self.weights[name]=1./len(signal_names)
875
+ return self.weights.copy()
876
+ class ModelSelector:
877
+ def __init__(self):self.model_performance=defaultdict(dict)
878
+ def select_best_model(self,regime:str,model_names:List[str])->str:
879
+ if regime not in self.model_performance:return model_names[0]if model_names else None
880
+ perf=self.model_performance[regime]
881
+ if not perf:return model_names[0]if model_names else None
882
+ best_model=max(perf.items(),key=lambda x:x[1]);return best_model[0]if best_model[1]>0 else model_names[0]
883
+ def update_performance(self,regime:str,model_name:str,performance:float):
884
+ if regime not in self.model_performance:self.model_performance[regime]={}
885
+ self.model_performance[regime][model_name]=performance
886
+ class MetaLearner:
887
+ def __init__(self):self.weight_optimizer=SignalWeightOptimizer();self.model_selector=ModelSelector()
888
+ def optimize(self,signal_names:List[str],recent_performance:Dict[str,float],regime:str,model_names:List[str])->Tuple[Dict[str,float],str]:return self.weight_optimizer.update_weights(signal_names,recent_performance),self.model_selector.select_best_model(regime,model_names)
889
+ class ModelFactory:
890
+ @staticmethod
891
+ def create_model(model_type:str,**kwargs):
892
+ if model_type=='linear'and SKLEARN_AVAILABLE:return LinearRegression(**kwargs)
893
+ elif model_type=='rf'and SKLEARN_AVAILABLE:return RandomForestRegressor(**kwargs)
894
+ elif model_type=='gb'and SKLEARN_AVAILABLE:return GradientBoostingRegressor(**kwargs)
895
+ elif model_type=='xgb'and XGBOOST_AVAILABLE:return xgb.XGBRegressor(**kwargs)
896
+ elif model_type=='lgb'and LIGHTGBM_AVAILABLE:return lgb.LGBMRegressor(**kwargs)
897
+ class TargetGenerator:
898
+ @staticmethod
899
+ def generate_forward_returns(data:pd.DataFrame,k:int=1,target_col:str='price')->pd.Series:return pd.Series()if target_col not in data.columns else data[target_col].shift(-k)/data[target_col]-1.
900
+ class EnsemblePredictor:
901
+ def __init__(self):self.models={};self.weights={}
902
+ def add_model(self,name:str,model:Any,weight:float=1.):self.models[name]=model;self.weights[name]=weight
903
+ def predict(self,X:np.ndarray)->np.ndarray:
904
+ predictions=[];total_weight=sum(self.weights.values())
905
+ for(name,model)in self.models.items():
906
+ try:pred=model.predict(X);weight=self.weights[name]/total_weight if total_weight>0 else 1./len(self.models);predictions.append(pred*weight)
907
+ except Exception:pass
908
+ return np.sum(predictions,axis=0)if predictions else np.zeros(X.shape[0])
909
+ def update_weights(self,weights:Dict[str,float]):self.weights.update(weights)
910
+ class CovarianceEstimator:
911
+ CovSize=lambda cov:cov.size if isinstance(cov,np.ndarray)else np.array(cov.values).size if isinstance(cov,pd.DataFrame)else 0;CovShape=lambda cov:cov.shape if isinstance(cov,np.ndarray)else np.array(cov.values).shape if isinstance(cov,pd.DataFrame)else(0,0)
912
+ @staticmethod
913
+ def ewma_covariance(returns:pd.DataFrame,alpha:float=.94)->np.ndarray:
914
+ if returns.empty:return np.array([])
915
+ returns_clean=returns.fillna(.0)
916
+ if len(returns_clean.columns)==1:return np.array([[returns_clean.ewm(alpha=alpha,adjust=False).var().iloc[-1,0]]])
917
+ cov=returns_clean.ewm(alpha=alpha,adjust=False).cov();n_assets=len(returns_clean.columns);return cov.iloc[-n_assets:,:].values
918
+ @staticmethod
919
+ def shrinkage_estimator(returns:pd.DataFrame,shrinkage:float=.5)->np.ndarray:
920
+ returns_clean=returns.fillna(.0);sample_cov=returns_clean.cov().values;n=len(returns_clean.columns)
921
+ if n==0:return np.array([])
922
+ return shrinkage*(np.eye(n)*np.trace(sample_cov)/n)+(1-shrinkage)*sample_cov
923
+ @staticmethod
924
+ def compute_cross_asset_covariance(returns_dict:Dict[str,pd.Series],lambda_param:float=.94)->pd.DataFrame:
925
+ if not returns_dict or len(returns_dict)<2:return pd.DataFrame()
926
+ returns_df=pd.DataFrame(returns_dict).dropna()
927
+ if returns_df.empty:return pd.DataFrame()
928
+ cov_matrix=CovarianceEstimator.ewma_covariance(returns_df,alpha=1-lambda_param);asset_symbols=list(returns_dict.keys());return pd.DataFrame(cov_matrix,index=asset_symbols,columns=asset_symbols)
929
+ class RiskManager:
930
+ @staticmethod
931
+ def compute_cvar(returns:np.ndarray,alpha:float=.05)->float:
932
+ if len(returns)==0:return .0
933
+ var=np.percentile(returns,alpha*100);return abs(returns[returns<=var].mean())
934
+ @staticmethod
935
+ def stress_test(returns:pd.DataFrame,scenarios:List[Dict[str,float]])->Dict[str,float]:
936
+ results={}
937
+ for(scenario_name,scenario_returns)in scenarios.items():
938
+ shocked_returns=returns.copy()
939
+ for(asset,shock)in scenario_returns.items():
940
+ if asset in shocked_returns.columns:shocked_returns[asset]+=shock
941
+ results[scenario_name]=shocked_returns.sum(axis=1).mean()
942
+ return results
943
+ class TransactionCostModel:
944
+ def __init__(self,spread_bps:float=5.,slippage_bps:float=1e1,market_impact_coef:float=.5,maker_fee:float=.001,taker_fee:float=.002):self.spread_bps=spread_bps;self.slippage_bps=slippage_bps;self.market_impact_coef=market_impact_coef;self.maker_fee=maker_fee;self.taker_fee=taker_fee
945
+ def estimate_total_cost(self,trade_size:float,trade_value:float,current_price:float,daily_volume:float=None,is_market_order:bool=True)->Dict[str,float]:spread_cost=trade_value*(self.spread_bps/1e4)*.5;slippage_cost=trade_value*(self.slippage_bps/1e4);impact_cost=trade_value*self.market_impact_coef*np.sqrt(trade_value/daily_volume)if daily_volume and daily_volume>0 else .0;fee_cost=trade_value*(self.taker_fee if is_market_order else self.maker_fee);total_cost=spread_cost+slippage_cost+impact_cost+fee_cost;return{'total':total_cost,'spread':spread_cost,'slippage':slippage_cost,'impact':impact_cost,'fee':fee_cost,'total_pct':total_cost/trade_value if trade_value>0 else .0}
946
+ def adjust_expected_return(self,expected_return:float,trade_value:float,current_price:float,daily_volume:float=None)->float:costs=self.estimate_total_cost(trade_size=trade_value/current_price if current_price>0 else .0,trade_value=trade_value,current_price=current_price,daily_volume=daily_volume);return expected_return-costs['total_pct']
947
+ class PositionSizer:
948
+ def __init__(self,target_vol:float=.02):self.target_vol=target_vol
949
+ def size_position(self,base_fraction:float,realized_vol:float,uncertainty_penalty:float=.0)->float:vol_scale=min(2.,max(.25,self.target_vol/max(realized_vol,1e-06)));return float(np.clip(base_fraction*vol_scale*(1.-.5*uncertainty_penalty),.0,1.))
950
+ class PortfolioOptimizer:
951
+ def __init__(self,risk_aversion:float=1.):self.risk_aversion=risk_aversion
952
+ @staticmethod
953
+ def _make_psd(covariance:np.ndarray,epsilon:float=1e-06)->np.ndarray:
954
+ n=covariance.shape[0];cov_psd=covariance+np.eye(n)*epsilon
955
+ try:eigenvals,eigenvecs=np.linalg.eigh(cov_psd);eigenvals=np.maximum(eigenvals,epsilon);cov_psd=eigenvecs@np.diag(eigenvals)@eigenvecs.T
956
+ except Exception:pass
957
+ return cov_psd
958
+ def optimize_cvar(self,expected_returns:np.ndarray,covariance:np.ndarray,cvar_constraint:float=.05,max_leverage:float=1.,sector_constraints:Dict[str,float]=None,asset_types:List[str]=None,cross_asset_constraints:Dict[str,float]=None)->np.ndarray:
959
+ n=len(expected_returns)
960
+ if CVXPY_AVAILABLE:
961
+ try:
962
+ cov_psd=self._make_psd(covariance);w=cp.Variable(n);mu=cp.Parameter(n);mu.value=expected_returns;objective=cp.Maximize(mu@w-self.risk_aversion*cp.quad_form(w,cov_psd));constraints=[cp.sum(w)<=max_leverage,cp.sum(cp.abs(w))<=max_leverage*2,w>=-max_leverage,w<=max_leverage]
963
+ if asset_types and cross_asset_constraints:
964
+ for(asset_type,max_exposure)in cross_asset_constraints.items():
965
+ type_mask=np.array([at==asset_type for at in asset_types])
966
+ if np.any(type_mask):constraints.append(cp.sum(cp.abs(w[type_mask]))<=max_exposure)
967
+ problem=cp.Problem(objective,constraints);problem.solve(solver=cp.ECOS,verbose=False)
968
+ if problem.status in['optimal','optimal_inaccurate']and w.value is not None:return w.value
969
+ except Exception:pass
970
+ try:
971
+ inv_cov=np.linalg.inv(covariance+np.eye(n)*1e-06);w=inv_cov@expected_returns;w_norm=np.sum(np.abs(w));w=w/w_norm*max_leverage if w_norm>1e-06 else np.sign(expected_returns)*max_leverage/n
972
+ if asset_types and cross_asset_constraints:
973
+ for(asset_type,max_exposure)in cross_asset_constraints.items():
974
+ type_mask=np.array([at==asset_type for at in asset_types])
975
+ if np.any(type_mask):
976
+ type_weight_sum=np.sum(np.abs(w[type_mask]))
977
+ if type_weight_sum>max_exposure:w[type_mask]*=max_exposure/type_weight_sum
978
+ return np.clip(w,-max_leverage,max_leverage)
979
+ except Exception:return np.sign(expected_returns)*max_leverage/n
980
+ def optimize_asset_allocation(self,expected_returns_dict:Dict[str,float],covariance_df:pd.DataFrame,constraints:Dict[str,Any]=None)->Dict[str,float]:
981
+ if not expected_returns_dict or covariance_df.empty:return{}
982
+ asset_symbols=list(expected_returns_dict.keys());missing_assets=set(asset_symbols)-set(covariance_df.index)
983
+ for asset in missing_assets:covariance_df.loc[asset]=.0;covariance_df[asset]=.0
984
+ covariance_df=covariance_df.reindex(index=asset_symbols,columns=asset_symbols);expected_returns=np.array([expected_returns_dict[symbol]for symbol in asset_symbols]);covariance=covariance_df.values;asset_types=constraints.get('asset_types',None)if constraints else None;cross_asset_constraints=constraints.get('cross_asset_constraints',None)if constraints else None;weights=self.optimize_cvar(expected_returns=expected_returns,covariance=covariance,max_leverage=constraints.get('max_leverage',1.)if constraints else 1.,asset_types=asset_types,cross_asset_constraints=cross_asset_constraints);return{symbol:float(weight)for(symbol,weight)in zip(asset_symbols,weights)}
985
+ class BacktestEngine:
986
+ def __init__(self,initial_capital:float=1e5,commission_rate:float=.001,slippage_rate:float=.0005):self.initial_capital=initial_capital;self.commission_rate=commission_rate;self.slippage_rate=slippage_rate;self.trades=[];self.equity_curve=[];self.daily_returns=[]
987
+ def run_backtest(self,agent:'QuantTradingAgent',start_date:datetime,end_date:datetime,train_window:int=60,test_window:int=7,step:int=7)->Dict[str,Any]:
988
+ self.trades=[];self.equity_curve=[];self.daily_returns=[];capital,position,entry_price=self.initial_capital,.0,.0;current_date,fold=start_date,0
989
+ while current_date<end_date:
990
+ fold+=1;train_start=current_date-timedelta(days=train_window);test_start,test_end=current_date,min(current_date+timedelta(days=test_window),end_date)
991
+ if test_start>=test_end:break
992
+ test_results=self._simulate_period(agent,train_start,current_date,test_start,test_end,capital,position,entry_price);capital,position,entry_price=test_results['final_capital'],test_results['final_position'],test_results['final_entry_price'];current_date+=timedelta(days=step)
993
+ metrics=self._calculate_metrics();return{'metrics':metrics,'trades':self.trades,'equity_curve':self.equity_curve,'daily_returns':self.daily_returns,'initial_capital':self.initial_capital,'final_capital':capital,'total_return':(capital-self.initial_capital)/self.initial_capital}
994
+ def _simulate_period(self,agent:'QuantTradingAgent',train_start:datetime,train_end:datetime,test_start:datetime,test_end:datetime,initial_capital:float,initial_position:float,initial_entry_price:float)->Dict[str,Any]:
995
+ capital,position,entry_price=initial_capital,initial_position,initial_entry_price;agent.days_back=(train_end-train_start).days;agent.fetch_data()
996
+ if agent.price_data.empty:return{'final_capital':capital,'final_position':position,'final_entry_price':entry_price}
997
+ if'date'in agent.price_data.columns:test_data=agent.price_data[(agent.price_data['date']>=test_start)&(agent.price_data['date']<=test_end)].copy()
998
+ else:
999
+ test_data=agent.price_data.copy()
1000
+ if hasattr(agent.price_data.index,'date'):test_data=test_data[(test_data.index>=test_start)&(test_data.index<=test_end)]
1001
+ if test_data.empty:return{'final_capital':capital,'final_position':position,'final_entry_price':entry_price}
1002
+ for(idx,row)in test_data.iterrows():
1003
+ current_price=row['price'];current_date=row['date']if'date'in row else row.name if hasattr(row,'name')and isinstance(row.name,datetime)else datetime.now()
1004
+ if'date'in agent.price_data.columns:agent.price_data=agent.price_data[agent.price_data['date']<=current_date]
1005
+ else:agent.price_data=agent.price_data[agent.price_data.index<=current_date]
1006
+ agent.current_price=current_price
1007
+ try:
1008
+ signals_df=agent.compute_signals()
1009
+ if signals_df.empty:continue
1010
+ signal_scores=agent.validate_signals();predictions,covariance,pred_metadata=agent.generate_predictions(signal_scores)
1011
+ if len(predictions)==0:continue
1012
+ latest_pred=predictions[-1]if len(predictions)>0 else .0;expected_returns=np.array([latest_pred]);cov_size=CovarianceEstimator.CovSize(covariance);portfolio_weights=agent.optimize_portfolio(expected_returns,covariance)if cov_size>0 else np.array([.0]);weight=portfolio_weights[0]if len(portfolio_weights)>0 else .0;signal='BUY'if weight>.1 else'SELL'if weight<-.1 else'HOLD'
1013
+ if signal!='HOLD':trade_result=self._simulate_trade(signal,weight,current_price,capital,position,entry_price);capital,position,entry_price=trade_result['capital'],trade_result['position'],trade_result['entry_price']
1014
+ except Exception as e:logger.debug(f"Error in backtest simulation at {current_date}: {e}");continue
1015
+ portfolio_value=capital+position*current_price;self.equity_curve.append({'date':current_date,'equity':portfolio_value,'capital':capital,'position':position})
1016
+ if len(self.equity_curve)>1:self.daily_returns.append((portfolio_value-self.equity_curve[-2]['equity'])/self.equity_curve[-2]['equity'])
1017
+ return{'final_capital':capital,'final_position':position,'final_entry_price':entry_price}
1018
+ def _simulate_trade(self,signal:str,weight:float,price:float,capital:float,position:float,entry_price:float)->Dict[str,float]:
1019
+ portfolio_value=capital+position*price
1020
+ if signal=='BUY'and weight>0:
1021
+ target_value=portfolio_value*min(abs(weight),.2);target_position=target_value/price;trade_size=target_position-position
1022
+ if trade_size>0:
1023
+ execution_price=price*(1+self.slippage_rate);trade_cost=trade_size*execution_price;commission=trade_cost*self.commission_rate;total_cost=trade_cost+commission
1024
+ if total_cost<=capital:capital-=total_cost;position+=trade_size;entry_price=execution_price;self.trades.append({'date':datetime.now(),'signal':signal,'price':execution_price,'size':trade_size,'commission':commission,'slippage':trade_size*price*self.slippage_rate})
1025
+ elif signal=='SELL'and weight<0:
1026
+ target_value=portfolio_value*min(abs(weight),.2);target_position=-target_value/price;trade_size=position-target_position
1027
+ if trade_size>0:
1028
+ execution_price=price*(1-self.slippage_rate);trade_proceeds=trade_size*execution_price;commission=trade_proceeds*self.commission_rate;net_proceeds=trade_proceeds-commission;capital+=net_proceeds;position-=trade_size
1029
+ if position==0:entry_price=.0
1030
+ self.trades.append({'date':datetime.now(),'signal':signal,'price':execution_price,'size':trade_size,'commission':commission,'slippage':trade_size*price*self.slippage_rate})
1031
+ return{'capital':capital,'position':position,'entry_price':entry_price}
1032
+ def _calculate_metrics(self)->Dict[str,float]:
1033
+ if not self.equity_curve:return{}
1034
+ equity_values=[e['equity']for e in self.equity_curve];returns=np.array(self.daily_returns)if self.daily_returns else np.array([])
1035
+ if len(returns)==0:return{}
1036
+ total_return=(equity_values[-1]-equity_values[0])/equity_values[0]if equity_values[0]>0 else .0;days=len(equity_values);annualized_return=(1+total_return)**(252/days)-1 if days>0 else .0;volatility=np.std(returns)*np.sqrt(252)if len(returns)>1 else .0;sharpe_ratio=annualized_return/volatility if volatility>0 else .0;downside_returns=returns[returns<0];downside_std=np.std(downside_returns)*np.sqrt(252)if len(downside_returns)>1 else .0;sortino_ratio=annualized_return/downside_std if downside_std>0 else .0;peak=equity_values[0];max_drawdown=.0
1037
+ for equity in equity_values:
1038
+ if equity>peak:peak=equity
1039
+ drawdown=(peak-equity)/peak if peak>0 else .0;max_drawdown=max(max_drawdown,drawdown)
1040
+ wins=sum(1 for t in self.trades if t.get('profit',0)>0)if self.trades else 0;win_rate=wins/len(self.trades)if len(self.trades)>0 else .0;calmar_ratio=annualized_return/max_drawdown if max_drawdown>0 else .0;return{'total_return':total_return,'annualized_return':annualized_return,'volatility':volatility,'sharpe_ratio':sharpe_ratio,'sortino_ratio':sortino_ratio,'max_drawdown':max_drawdown,'calmar_ratio':calmar_ratio,'win_rate':win_rate,'total_trades':len(self.trades),'num_days':days}
1041
+ def _generate_report(self,results:Dict[str,Any])->str:metrics=results.get('metrics',{});return'\n'.join(['='*80,'BACKTEST REPORT','='*80,f"\nInitial Capital: ${results.get("initial_capital",0):,.2f}",f"Final Capital: ${results.get("final_capital",0):,.2f}",f"Total Return: {results.get("total_return",0):.2%}",f"\nPerformance Metrics:",f" Annualized Return: {metrics.get("annualized_return",0):.2%}",f" Volatility: {metrics.get("volatility",0):.2%}",f" Sharpe Ratio: {metrics.get("sharpe_ratio",0):.2f}",f" Sortino Ratio: {metrics.get("sortino_ratio",0):.2f}",f" Max Drawdown: {metrics.get("max_drawdown",0):.2%}",f" Calmar Ratio: {metrics.get("calmar_ratio",0):.2f}",f" Win Rate: {metrics.get("win_rate",0):.2%}",f" Total Trades: {metrics.get("total_trades",0)}",'='*80])
1042
+ class CircuitBreaker:
1043
+ def __init__(self,max_daily_loss:float=.05,max_trades_per_day:int=50,kill_switch_file:str='kill_switch.txt'):self.max_daily_loss=max_daily_loss;self.max_trades_per_day=max_trades_per_day;self.kill_switch_file=kill_switch_file;self.daily_pnl=.0;self.daily_trades=0;self.last_reset_date=datetime.now().date();self.is_tripped=False
1044
+ def check_circuit(self)->Tuple[bool,str]:
1045
+ current_date=datetime.now().date()
1046
+ if current_date!=self.last_reset_date:self.daily_pnl=.0;self.daily_trades=0;self.last_reset_date=current_date
1047
+ if os.path.exists(self.kill_switch_file):self.is_tripped=True;return False,'Kill switch activated'
1048
+ if self.daily_pnl<=-self.max_daily_loss:self.is_tripped=True;return False,f"Daily loss limit exceeded: {self.daily_pnl:.2%}"
1049
+ if self.daily_trades>=self.max_trades_per_day:self.is_tripped=True;return False,f"Daily trade limit exceeded: {self.daily_trades}"
1050
+ return True,'OK'
1051
+ def record_trade(self,pnl:float):self.daily_pnl+=pnl;self.daily_trades+=1
1052
+ def reset(self):self.is_tripped=False;self.daily_pnl=.0;self.daily_trades=0
1053
+ class RiskMonitor:
1054
+ def __init__(self,max_position_size:float=.2,max_leverage:float=1.,max_correlation:float=.8,max_dollar_risk_per_trade:float=None,max_portfolio_volatility:float=.2,max_drawdown:float=.15,max_exposure_per_asset:float=.15,max_exposure_per_type:Dict[str,float]=None):self.max_position_size=max_position_size;self.max_leverage=max_leverage;self.max_correlation=max_correlation;self.max_dollar_risk_per_trade=max_dollar_risk_per_trade;self.max_portfolio_volatility=max_portfolio_volatility;self.max_drawdown=max_drawdown;self.max_exposure_per_asset=max_exposure_per_asset;self.max_exposure_per_type=max_exposure_per_type or{};self.peak_portfolio_value=None;self.current_drawdown=.0
1055
+ def check_dollar_risk(self,position_size:float,stop_loss_distance:float,portfolio_value:float)->Tuple[bool,float]:
1056
+ if self.max_dollar_risk_per_trade is None:return True,position_size
1057
+ dollar_risk=position_size*portfolio_value*stop_loss_distance
1058
+ if dollar_risk>self.max_dollar_risk_per_trade:adjusted_size=self.max_dollar_risk_per_trade/(portfolio_value*stop_loss_distance);return False,adjusted_size
1059
+ return True,position_size
1060
+ def check_volatility_risk(self,position_size:float,asset_volatility:float,portfolio_volatility:float=None)->Tuple[bool,float]:
1061
+ if asset_volatility>0 and asset_volatility>self.max_portfolio_volatility:vol_adjusted_size=position_size*(self.max_portfolio_volatility/asset_volatility);return False,vol_adjusted_size
1062
+ if portfolio_volatility and portfolio_volatility>self.max_portfolio_volatility:scale_factor=self.max_portfolio_volatility/portfolio_volatility;vol_adjusted_size=position_size*scale_factor;return False,vol_adjusted_size
1063
+ return True,position_size
1064
+ def check_exposure_limits(self,position_size:float,asset_type:str=None,current_exposures:Dict[str,float]=None)->Tuple[bool,float]:
1065
+ if abs(position_size)>self.max_exposure_per_asset:adjusted_size=np.sign(position_size)*self.max_exposure_per_asset;return False,adjusted_size
1066
+ if asset_type and asset_type in self.max_exposure_per_type:
1067
+ max_type_exposure=self.max_exposure_per_type[asset_type]
1068
+ if current_exposures:
1069
+ current_type_exposure=sum(exp for(asset,exp)in current_exposures.items()if self._get_asset_type(asset)==asset_type)
1070
+ if current_type_exposure+abs(position_size)>max_type_exposure:remaining=max_type_exposure-current_type_exposure;adjusted_size=np.sign(position_size)*max(0,remaining);return False,adjusted_size
1071
+ return True,position_size
1072
+ def check_drawdown_limit(self,current_portfolio_value:float)->Tuple[bool,float]:
1073
+ if self.peak_portfolio_value is None:self.peak_portfolio_value=current_portfolio_value
1074
+ if current_portfolio_value>self.peak_portfolio_value:self.peak_portfolio_value=current_portfolio_value
1075
+ drawdown=(self.peak_portfolio_value-current_portfolio_value)/self.peak_portfolio_value;self.current_drawdown=drawdown
1076
+ if drawdown>self.max_drawdown:reduction_factor=1.-(drawdown-self.max_drawdown)/drawdown;reduction_factor=max(.1,reduction_factor);return False,reduction_factor
1077
+ return True,1.
1078
+ def _get_asset_type(self,asset_symbol:str)->str:
1079
+ asset_upper=asset_symbol.upper()
1080
+ if asset_upper in['BTC','ETH','DOGE','XRP','DOT','MATIC']:return'crypto'
1081
+ return'crypto'
1082
+ def pre_trade_check(self,signal:str,position_size:float,current_positions:Dict[str,float],portfolio_value:float,stop_loss_distance:float=.02,asset_volatility:float=None,asset_type:str=None)->Tuple[bool,str,float]:
1083
+ adjusted_size=position_size
1084
+ if abs(position_size)>self.max_position_size:adjusted_size=np.sign(position_size)*self.max_position_size;return False,f"Position size {position_size:.2%} exceeds limit {self.max_position_size:.2%}",adjusted_size
1085
+ if self.max_dollar_risk_per_trade:
1086
+ is_valid,adjusted=self.check_dollar_risk(position_size,stop_loss_distance,portfolio_value)
1087
+ if not is_valid:adjusted_size=min(abs(adjusted_size),adjusted)*np.sign(adjusted_size);return False,f"Dollar risk exceeds limit ${self.max_dollar_risk_per_trade:.2f}",adjusted_size
1088
+ if asset_volatility:
1089
+ is_valid,adjusted=self.check_volatility_risk(position_size,asset_volatility)
1090
+ if not is_valid:adjusted_size=min(abs(adjusted_size),abs(adjusted))*np.sign(adjusted_size);return False,f"Volatility risk exceeds limit {self.max_portfolio_volatility:.0%}",adjusted_size
1091
+ current_exposures={k:abs(v)for(k,v)in current_positions.items()};is_valid,adjusted=self.check_exposure_limits(position_size,asset_type,current_exposures)
1092
+ if not is_valid:adjusted_size=adjusted;return False,f"Exposure limit exceeded: {self.max_exposure_per_asset:.0%}",adjusted_size
1093
+ total_exposure=sum(abs(p)for p in current_positions.values())+abs(position_size)
1094
+ if total_exposure>self.max_leverage:return False,f"Total leverage {total_exposure:.2f} exceeds limit {self.max_leverage:.2f}",.0
1095
+ is_valid,reduction=self.check_drawdown_limit(portfolio_value)
1096
+ if not is_valid:adjusted_size*=reduction;return False,f"Drawdown limit exceeded: {self.max_drawdown:.0%} (current: {self.current_drawdown:.0%})",adjusted_size
1097
+ return True,'OK',adjusted_size
1098
+ def monitor_risk(self,positions:Dict[str,float],portfolio_value:float,covariance:np.ndarray)->Dict[str,float]:
1099
+ position_vector=np.array(list(positions.values()));cov_size,cov_shape=CovarianceEstimator.CovSize(covariance),CovarianceEstimator.CovShape(covariance)
1100
+ if cov_size>0 and len(position_vector)==cov_shape[0]:portfolio_variance=position_vector@covariance@position_vector;portfolio_risk=np.sqrt(portfolio_variance)
1101
+ else:portfolio_risk=.0
1102
+ return{'portfolio_risk':portfolio_risk,'total_exposure':sum(abs(p)for p in positions.values()),'num_positions':len(positions)}
1103
+ class ConfidenceCalibrator:
1104
+ def __init__(self):(self.calibration_data):List[Tuple[float,bool]]=[];self.isotonic_model=None;(self.quantile_models):Dict[str,Any]={};(self.brier_scores):List[float]=[]
1105
+ def add_observation(self,predicted_prob:float,actual_in_interval:bool)->None:
1106
+ self.calibration_data.append((predicted_prob,actual_in_interval))
1107
+ if len(self.calibration_data)>1000:self.calibration_data=self.calibration_data[-1000:]
1108
+ def calibrate(self)->None:
1109
+ if len(self.calibration_data)<20:return
1110
+ try:
1111
+ if SKLEARN_AVAILABLE:from sklearn.isotonic import IsotonicRegression;probs=np.array([d[0]for d in self.calibration_data]);outcomes=np.array([float(d[1])for d in self.calibration_data]);self.isotonic_model=IsotonicRegression(out_of_bounds='clip');self.isotonic_model.fit(probs,outcomes)
1112
+ except Exception:pass
1113
+ def calibrate_prob(self,raw_prob:float)->float:
1114
+ if self.isotonic_model is None:return raw_prob
1115
+ try:return float(self.isotonic_model.predict([raw_prob])[0])
1116
+ except Exception:return raw_prob
1117
+ def compute_brier_score(self,predicted_probs:List[float],actual_outcomes:List[bool])->float:
1118
+ if len(predicted_probs)!=len(actual_outcomes)or len(predicted_probs)==0:return 1.
1119
+ brier=np.mean([(p-float(a))**2 for(p,a)in zip(predicted_probs,actual_outcomes)]);self.brier_scores.append(brier)
1120
+ if len(self.brier_scores)>100:self.brier_scores=self.brier_scores[-100:]
1121
+ return float(brier)
1122
+ def get_calibration_metrics(self)->Dict[str,float]:
1123
+ if len(self.brier_scores)==0:return{'brier_score':1.,'calibration_samples':0}
1124
+ return{'brier_score':float(np.mean(self.brier_scores)),'calibration_samples':len(self.calibration_data),'recent_brier':self.brier_scores[-1]if self.brier_scores else 1.}
1125
+ class MonitoringSystem:
1126
+ def __init__(self):self.pnl_history=[];self.signal_health={};self.crca_diagnostics={};self.performance_metrics={}
1127
+ def track_pnl(self,timestamp:datetime,pnl:float,position:float,price:float):self.pnl_history.append({'timestamp':timestamp,'pnl':pnl,'position':position,'price':price})
1128
+ def monitor_signal_health(self,signal_name:str,score:float,decay:float):self.signal_health[signal_name]={'score':score,'decay':decay,'last_update':datetime.now()}
1129
+ def update_crca_diagnostics(self,graph_health:Dict[str,float],edge_strengths:Dict[str,float]):self.crca_diagnostics={'graph_health':graph_health,'edge_strengths':edge_strengths,'last_update':datetime.now()}
1130
+ def calculate_performance_attribution(self,signals:Dict[str,float],returns:pd.Series)->Dict[str,float]:
1131
+ attribution={}
1132
+ for(signal_name,signal_values)in signals.items():
1133
+ if len(signal_values)==len(returns):correlation=signal_values.corr(returns);attribution[signal_name]=correlation
1134
+ self.performance_metrics['attribution']=attribution;return attribution
1135
+ def get_summary(self)->Dict[str,Any]:total_pnl=sum(p['pnl']for p in self.pnl_history)if self.pnl_history else .0;return{'total_pnl':total_pnl,'num_trades':len(self.pnl_history),'signal_health':self.signal_health,'crca_diagnostics':self.crca_diagnostics,'performance_metrics':self.performance_metrics}
1136
+ class TWAPExecutor:
1137
+ def execute(self,total_size:float,duration_minutes:int=60,num_splits:int=10)->List[Tuple[float,float]]:
1138
+ split_size=total_size/num_splits;interval=duration_minutes/num_splits;splits=[]
1139
+ for i in range(num_splits):splits.append((split_size,i*interval*60))
1140
+ return splits
1141
+ class VWAPExecutor:
1142
+ def execute(self,total_size:float,volume_profile:pd.Series,duration_minutes:int=60)->List[Tuple[float,float]]:
1143
+ if volume_profile.empty:executor=TWAPExecutor();return executor.execute(total_size,duration_minutes)
1144
+ normalized_vol=volume_profile/volume_profile.sum();num_splits=len(normalized_vol);splits=[]
1145
+ for(i,vol_weight)in enumerate(normalized_vol):split_size=total_size*vol_weight;time_offset=duration_minutes/num_splits*i*60;splits.append((split_size,time_offset))
1146
+ return splits
1147
+ class SmartRouter:
1148
+ def __init__(self):self.exchanges={}
1149
+ def route_order(self,symbol:str,side:str,size:float,exchanges:List[str])->List[Dict[str,Any]]:
1150
+ size_per_exchange=size/len(exchanges);orders=[]
1151
+ for exchange in exchanges:orders.append({'exchange':exchange,'symbol':symbol,'side':side,'size':size_per_exchange})
1152
+ return orders
1153
+ def iceberg_order(self,total_size:float,visible_size:float,num_slices:int=5)->List[Tuple[float,float]]:
1154
+ slices=[];remaining=total_size
1155
+ for i in range(num_slices):
1156
+ slice_size=min(visible_size,remaining)
1157
+ if slice_size>0:slices.append((slice_size,i*60));remaining-=slice_size
1158
+ return slices
1159
+ class ImplementationShortfallOptimizer:
1160
+ def optimize_order_split(self,total_size:float,arrival_price:float,current_price:float,volatility:float,time_horizon:int=60)->List[Tuple[float,float]]:
1161
+ num_splits=min(10,time_horizon//6);split_size=total_size/num_splits;splits=[]
1162
+ for i in range(num_splits):splits.append((split_size,i*6))
1163
+ return splits
1164
+ class KrakenRestClient:
1165
+ def __init__(self,api_key:str,api_secret:str,base_url:str='https://api.kraken.com',timeout:int=10,max_retries:int=3,dry_run:bool=False)->None:self.api_key=api_key;self.api_secret=api_secret;self.base_url=base_url.rstrip('/');self.timeout=timeout;self.max_retries=max_retries;self.dry_run=dry_run;self._min_interval=.35;(self._last_call_ts):float=.0
1166
+ def _throttle(self)->None:
1167
+ now=time.time();wait=self._min_interval-(now-self._last_call_ts)
1168
+ if wait>0:
1169
+ try:time.sleep(wait)
1170
+ except Exception:pass
1171
+ self._last_call_ts=time.time()
1172
+ def _sign(self,path:str,data:Dict[str,Any])->Dict[str,str]:nonce=data.get('nonce')or str(int(time.time()*1000));data['nonce']=nonce;post_data=urlencode(data);message=(nonce+post_data).encode();sha256=hashlib.sha256(message).digest();mac_data=path.encode()+sha256;secret=base64.b64decode(self.api_secret);sig=hmac.new(secret,mac_data,hashlib.sha512).digest();api_sign=base64.b64encode(sig).decode();return{'API-Key':self.api_key,'API-Sign':api_sign}
1173
+ def _post(self,path:str,data:Dict[str,Any])->Dict[str,Any]:
1174
+ url=f"{self.base_url}{path}";base_payload:Dict[str,Any]=dict(data)if data else{};last_error:Optional[str]=None
1175
+ for attempt in range(max(1,self.max_retries)):
1176
+ payload:Dict[str,Any]=dict(base_payload)
1177
+ try:
1178
+ self._throttle()
1179
+ if self.dry_run:logger.info(f"[Kraken REST dry-run] POST {path} payload={payload}");return{'success':True,'error':None,'result':{'dry_run':True,'request':{'path':path,'data':payload}}}
1180
+ headers=self._sign(path,payload);post_data=urlencode(payload);cmd=['curl','-s','-X','POST',url,'-H',f"API-Key: {headers["API-Key"]}",'-H',f"API-Sign: {headers["API-Sign"]}",'--data',post_data];proc=subprocess.run(cmd,capture_output=True,text=True,timeout=self.timeout)
1181
+ if proc.returncode!=0:last_error=proc.stderr.strip()or f"curl exited with {proc.returncode}";logger.warning(f"Kraken REST request failed ({attempt+1}/{self.max_retries}) on {path}: {last_error}");time.sleep(min(1.5**attempt,5.));continue
1182
+ raw=proc.stdout.strip()
1183
+ if not raw:last_error='empty response';logger.warning(f"Kraken REST request failed ({attempt+1}/{self.max_retries}) on {path}: {last_error}");time.sleep(min(1.5**attempt,5.));continue
1184
+ body=json.loads(raw);errors=body.get('error')or[]
1185
+ if errors:msg='; '.join(errors);logger.error(f"Kraken REST error on {path}: {msg}");return{'success':False,'error':msg,'result':body.get('result')or{}}
1186
+ return{'success':True,'error':None,'result':body.get('result')or{}}
1187
+ except Exception as exc:
1188
+ last_error=str(exc);logger.warning(f"Kraken REST request failed ({attempt+1}/{self.max_retries}) on {path}: {exc}")
1189
+ try:time.sleep(min(1.5**attempt,5.))
1190
+ except Exception:continue
1191
+ return{'success':False,'error':last_error or'unknown error','result':{}}
1192
+ def get_balance(self)->Dict[str,Any]:return self._post('/0/private/Balance',{})
1193
+ def add_order(self,pair:str,side:str,volume:float,ordertype:str='market',price:Optional[float]=None,leverage:Optional[Union[int,float]]=None,oflags:Optional[str]=None,**extra_params:Any)->Dict[str,Any]:
1194
+ data:Dict[str,Any]={'pair':pair,'type':side,'ordertype':ordertype,'volume':f"{volume:.10f}"}
1195
+ if price is not None and ordertype!='market':data['price']=f"{price:.10f}"
1196
+ if leverage is not None and float(leverage)>1.:data['leverage']=str(leverage)
1197
+ if oflags:data['oflags']=oflags
1198
+ if extra_params:data.update(extra_params)
1199
+ return self._post('/0/private/AddOrder',data)
1200
+ class ExecutionEngine:
1201
+ def __init__(self,exchange_name:str='coinbase',api_key:Optional[str]=None,api_secret:Optional[str]=None,api_passphrase:Optional[str]=None,testnet:bool=True,max_position_size:float=.1,require_confirmation:bool=True,dry_run:bool=False):
1202
+ self.exchange_name=exchange_name.lower();self.testnet=testnet;self.max_position_size=max_position_size;self.require_confirmation=require_confirmation;self.exchange=None;self.position=.0;self.available_balance=.0;self.dry_run=dry_run;self.quote_preferences=TRADING_CONFIG.get('quote_preferences',{'kraken':['EUR','USD','USDC','USDT'],'binance':['USDT','BUSD','USD','USDC'],'coinbase':['USD','USDC','USDT'],'default':['USDT','USD','USDC']});self.is_optimizer=ImplementationShortfallOptimizer();(self.kraken_rest):Optional[KrakenRestClient]=None;self.trade_lock=threading.Lock()
1203
+ if not CCXT_AVAILABLE:logger.warning('CCXT not available - live trading disabled');return
1204
+ self._initialize_exchange(api_key,api_secret,api_passphrase)
1205
+ def _initialize_exchange(self,api_key:Optional[str],api_secret:Optional[str],api_passphrase:Optional[str])->None:
1206
+ exchange_upper=self.exchange_name.upper();api_key=api_key or os.getenv(f"{exchange_upper}_API_KEY")or(KRAKEN_API_KEY_FALLBACK if self.exchange_name=='kraken'else None);api_secret=api_secret or os.getenv(f"{exchange_upper}_API_SECRET")or(KRAKEN_API_SECRET_FALLBACK if self.exchange_name=='kraken'else None);api_passphrase=api_passphrase or(os.getenv(f"{exchange_upper}_API_PASSPHRASE")if self.exchange_name in['coinbase','kraken']else None)or(KRAKEN_API_PASSPHRASE_FALLBACK if self.exchange_name=='kraken'else None)
1207
+ if not api_key or not api_secret:logger.warning('Exchange credentials not found - live trading disabled');return
1208
+ try:
1209
+ config={'apiKey':api_key,'secret':api_secret,'enableRateLimit':True,'options':{'defaultType':'spot'}}
1210
+ if api_passphrase:config['passphrase']=api_passphrase
1211
+ if self.testnet and self.exchange_name.lower()!='kraken':config['sandbox'],config['options']['sandboxMode']=True,True
1212
+ elif self.testnet and self.exchange_name.lower()=='kraken':logger.warning("Kraken doesn't support sandbox mode - using production API (testnet disabled)");self.testnet=False
1213
+ self.exchange=getattr(ccxt,self.exchange_name)(config);logger.info(f"Exchange {self.exchange_name} initialized (testnet={self.testnet})")
1214
+ try:self.markets=self.exchange.load_markets()
1215
+ except Exception as e:logger.warning(f"Failed to load markets for {self.exchange_name}: {e}");self.markets={}
1216
+ if self.exchange_name=='kraken':self.kraken_rest=KrakenRestClient(api_key=api_key,api_secret=api_secret,dry_run=self.dry_run or self.testnet)
1217
+ self._update_balance()
1218
+ except Exception as e:logger.error(f"Failed to initialize exchange: {e}")
1219
+ def _update_balance(self,base_symbol:str='ETH')->None:
1220
+ if self.exchange_name=='kraken'and self.kraken_rest is not None:
1221
+ try:
1222
+ resp=self.kraken_rest.get_balance()
1223
+ if resp.get('success'):
1224
+ raw_balances:Dict[str,Any]=resp.get('result')or{};normalized:Dict[str,float]={}
1225
+ for(asset_code,qty)in raw_balances.items():
1226
+ norm=self._normalize_kraken_asset(str(asset_code))
1227
+ try:normalized[norm]=normalized.get(norm,.0)+float(qty)
1228
+ except(TypeError,ValueError):continue
1229
+ self.latest_balances_norm=normalized;quotes=getattr(self,'quote_preferences',None)
1230
+ if isinstance(quotes,dict):quote_list=quotes.get(self.exchange_name,quotes.get('default',['USDT','USD','USDC','EUR']))
1231
+ else:quote_list=quotes or['USDT','USD','USDC','EUR']
1232
+ self.available_balance=.0
1233
+ for q in quote_list:
1234
+ if q in normalized:self.available_balance=float(normalized[q]);break
1235
+ self.available_balances_by_quote={q:normalized.get(q,.0)for q in quote_list if q in normalized};base_norm=(base_symbol or'ETH').upper()
1236
+ if base_norm=='BTC':base_norm='BTC'
1237
+ self.position=float(normalized.get(base_norm,.0));return
1238
+ except Exception as e:logger.warning(f"Kraken REST balance fetch failed, falling back to CCXT: {e}")
1239
+ if not self.exchange:return
1240
+ try:
1241
+ balance=self.exchange.fetch_balance()
1242
+ try:self.latest_balances_norm={k.upper():float(v.get('free',.0)or .0)for(k,v)in balance.items()if isinstance(v,dict)}
1243
+ except Exception:pass
1244
+ self.available_balance=float(balance.get('USDT',{}).get('free',.0)or balance.get('USD',{}).get('free',.0)or balance.get('USDC',{}).get('free',.0)or balance.get('EUR',{}).get('free',.0)or .0);self.position=float(balance.get(base_symbol.upper(),{}).get('free',.0)or .0)
1245
+ except Exception as e:logger.warning(f"Failed to fetch balance: {e}")
1246
+ @staticmethod
1247
+ def _normalize_kraken_asset(asset_code:str)->str:
1248
+ if not asset_code:return''
1249
+ code=asset_code.split('.',1)[0].upper()
1250
+ if len(code)>3 and code[0]in('X','Z'):code=code[1:]
1251
+ if code=='XBT':code='BTC'
1252
+ if code=='ZUSD':code='USD'
1253
+ if code=='ZEUR':code='EUR'
1254
+ return code
1255
+ def _derive_symbol(self,base_symbol:str)->Optional[str]:
1256
+ if not base_symbol:return
1257
+ base=base_symbol.upper();bases_to_try=[base]
1258
+ if self.exchange_name=='kraken'and base=='BTC':bases_to_try.append('XBT')
1259
+ exchange_quotes=getattr(self,'quote_preferences',None)
1260
+ if not exchange_quotes:exchange_quotes=['USDT','USD','USDC']
1261
+ if isinstance(exchange_quotes,dict):exchange_quotes=exchange_quotes.get(self.exchange_name,exchange_quotes.get('default',['USDT','USD','USDC']))
1262
+ for b in bases_to_try:
1263
+ for quote in exchange_quotes:
1264
+ pair=f"{b}/{quote}"
1265
+ try:
1266
+ if self.exchange and pair in getattr(self,'markets',{}):return pair
1267
+ except Exception:pass
1268
+ def get_live_holdings(self)->Dict[str,float]:
1269
+ holdings:Dict[str,float]={}
1270
+ if hasattr(self,'latest_balances_norm')and isinstance(self.latest_balances_norm,dict):
1271
+ for(k,v)in self.latest_balances_norm.items():
1272
+ try:
1273
+ val=float(v)
1274
+ if abs(val)>0:holdings[k]=val
1275
+ except Exception:continue
1276
+ return holdings
1277
+ def _with_retries(self,fn,*args,retries:int=3,**kwargs):
1278
+ last_exc=None;delay=max(getattr(self.exchange,'rateLimit',250)/1e3,.25)if self.exchange else .25
1279
+ for _ in range(max(1,retries)):
1280
+ try:return fn(*args,**kwargs)
1281
+ except Exception as e:
1282
+ last_exc=e
1283
+ try:import time;time.sleep(delay);delay*=1.5
1284
+ except Exception:pass
1285
+ if last_exc:raise last_exc
1286
+ def _market_meta(self,routed_symbol:str)->Dict[str,Any]:
1287
+ try:
1288
+ if self.exchange and routed_symbol in self.exchange.markets:return self.exchange.markets[routed_symbol]
1289
+ except Exception:pass
1290
+ return{}
1291
+ def execute_trade(self,signal:str,size_fraction:float=.1,price:Optional[float]=None,use_is:bool=False,base_symbol:str='ETH',aggressive_mode:bool=False,expected_return_pct:Optional[float]=None,leverage:Optional[Union[int,float]]=None)->Dict[str,Any]:
1292
+ lock_acquired=False
1293
+ if hasattr(self,'trade_lock'):
1294
+ lock_acquired=self.trade_lock.acquire(timeout=10)
1295
+ if not lock_acquired:return{'error':'Trade lock timeout'}
1296
+ try:
1297
+ if not self.exchange and not(self.exchange_name=='kraken'and self.kraken_rest is not None):return{'error':'Exchange not initialized'}
1298
+ if signal=='HOLD':return{'status':'hold'}
1299
+ self._update_balance(base_symbol);size_fraction=min(abs(size_fraction),self.max_position_size);routed_symbol=self._derive_symbol(base_symbol)or f"{base_symbol.upper()}/USDT";market=self._market_meta(routed_symbol);lot_min=market.get('limits',{}).get('amount',{}).get('min',None);lot_max=market.get('limits',{}).get('amount',{}).get('max',None);notional_min=market.get('limits',{}).get('cost',{}).get('min',None);price_precision=market.get('precision',{}).get('price',None);step=market.get('precision',{}).get('amount',None);price_info=None
1300
+ try:
1301
+ if self.exchange:
1302
+ if price is None:price_info=self._with_retries(self.exchange.fetch_ticker,routed_symbol);price=float(price_info['last'])
1303
+ else:price_info=self._with_retries(self.exchange.fetch_ticker,routed_symbol)
1304
+ elif price is None:return{'error':f"Ticker fetch failed for {routed_symbol} (no metadata client available)"}
1305
+ except Exception:return{'error':f"Ticker fetch failed for {routed_symbol}"}
1306
+ if price is None or price<=0:return{'error':f"Invalid price for {routed_symbol}: {price}"}
1307
+ try:
1308
+ ref_price=None
1309
+ if price_info:
1310
+ bid,ask,last=price_info.get('bid'),price_info.get('ask'),price_info.get('last')
1311
+ if bid and ask:ref_price=(bid+ask)/2
1312
+ elif last:ref_price=last
1313
+ ref_price=ref_price or price
1314
+ if ref_price and ref_price>0:
1315
+ slip_pct=abs(price-ref_price)/ref_price
1316
+ if slip_pct>.005:return{'error':f"Slippage {slip_pct:.2%} exceeds 0.5% cap"}
1317
+ except Exception:pass
1318
+ if price_precision is not None:
1319
+ try:quant=10**-price_precision;price=float(np.floor(price/quant)*quant)
1320
+ except Exception:pass
1321
+ quote_ccy=routed_symbol.split('/')[1]if'/'in routed_symbol else None;quote_balance=self.available_balance
1322
+ if self.exchange_name=='kraken'and quote_ccy and hasattr(self,'available_balances_by_quote'):
1323
+ qb=self.available_balances_by_quote.get(quote_ccy)
1324
+ if qb is not None:quote_balance=float(qb)
1325
+ min_trade_value=getattr(self,'min_trade_value',5.)if hasattr(self,'min_trade_value')else 5.
1326
+ if signal=='BUY':
1327
+ if quote_balance<=0:return{'status':'skipped','reason':'no available balance'}
1328
+ required_fraction=min_trade_value*1.02/quote_balance;size_fraction=max(size_fraction,required_fraction)
1329
+ if self.max_position_size:size_fraction=min(size_fraction,self.max_position_size)
1330
+ notional=quote_balance*size_fraction
1331
+ if notional<min_trade_value:return{'status':'skipped','reason':f"notional {notional:.2f} < min_trade_value {min_trade_value:.2f}"}
1332
+ amount,side=notional/price,'buy'
1333
+ elif signal=='SELL':
1334
+ base_free=self.position
1335
+ if base_free<=0:return{'status':'skipped','reason':f"no position in {base_symbol}"}
1336
+ if base_free*price<=0:return{'status':'skipped','reason':'price or position non-positive'}
1337
+ required_fraction=min_trade_value*1.02/(base_free*price);size_fraction=max(size_fraction,required_fraction)
1338
+ if self.max_position_size:size_fraction=min(size_fraction,self.max_position_size)
1339
+ amount=base_free*size_fraction;notional,side=amount*price,'sell'
1340
+ if notional<min_trade_value:
1341
+ target_amount=min_trade_value*1.02/price;amount=min(base_free,target_amount);notional=amount*price
1342
+ if notional<min_trade_value:return{'status':'skipped','reason':f"notional {notional:.2f} < min_trade_value {min_trade_value:.2f} (after bump)"}
1343
+ else:return{'error':f"Invalid signal: {signal}"}
1344
+ def clamp_to_step(val:float,step_val:Optional[float])->float:
1345
+ if step_val and step_val>0:return float(np.floor(val/step_val)*step_val)
1346
+ return val
1347
+ if lot_min is not None and amount<lot_min:
1348
+ if aggressive_mode:amount=clamp_to_step(lot_min,step)
1349
+ else:return{'status':'skipped','reason':f"amount {amount} < min {lot_min}"}
1350
+ if step:amount=clamp_to_step(amount,step)
1351
+ if lot_min is not None and amount<lot_min:return{'status':'skipped','reason':f"amount {amount} < min {lot_min}"}
1352
+ if lot_max is not None and amount>lot_max:amount=min(amount,lot_max)
1353
+ notional=price*amount
1354
+ if notional_min is not None and notional<notional_min:return{'status':'skipped','reason':f"notional {notional:.4f} < min {notional_min}"}
1355
+ taker_fee=market.get('taker',.001)if isinstance(market,dict)else .001
1356
+ if expected_return_pct is not None:
1357
+ total_cost_pct=taker_fee+.005
1358
+ if expected_return_pct<=total_cost_pct:return{'status':'skipped','reason':f"expected_return {expected_return_pct:.2%} <= fees+slip {total_cost_pct:.2%}"}
1359
+ if use_is and amount>.01:splits=self.is_optimizer.optimize_order_split(amount,price,price,.02);logger.info(f"IS optimization: {len(splits)} splits");amount=splits[0][0]if splits else amount
1360
+ using_kraken_rest=self.exchange_name=='kraken'and self.kraken_rest is not None;pair_id=None
1361
+ if using_kraken_rest and isinstance(market,dict):pair_id=market.get('id')
1362
+ if using_kraken_rest and not pair_id:pair_id=routed_symbol.replace('/','')
1363
+ if using_kraken_rest:
1364
+ if self.dry_run:logger.info(f"[Kraken REST] {side.upper()} {amount:.6f} {pair_id} @ {price:.6f} (leverage={leverage or 1.}, dry_run={self.dry_run})")
1365
+ resp=self.kraken_rest.add_order(pair=pair_id,side=side,volume=amount,ordertype='market',price=None,leverage=leverage)
1366
+ if not resp.get('success'):
1367
+ err_msg=(resp.get('error')or'').upper()
1368
+ if'USDT'in(pair_id or'').upper()and'RESTRICT'in err_msg:
1369
+ base=routed_symbol.split('/')[0]
1370
+ for alt_quote in['EUR','USD','USDC']:
1371
+ alt_pair=f"{base}/{alt_quote}";market_alt=getattr(self,'markets',{}).get(alt_pair)if getattr(self,'markets',None)else None;alt_pair_id=market_alt.get('id')if isinstance(market_alt,dict)and market_alt.get('id')else alt_pair.replace('/','');logger.warning(f"USDT restricted on Kraken; retrying on {alt_pair_id}");resp_alt=self.kraken_rest.add_order(pair=alt_pair_id,side=side,volume=amount,ordertype='market',price=None,leverage=leverage)
1372
+ if resp_alt.get('success'):result_alt=resp_alt.get('result')or{};txids_alt=result_alt.get('txid')or[];order_id_alt=txids_alt[0]if isinstance(txids_alt,list)and txids_alt else txids_alt if isinstance(txids_alt,str)else None;fill_cost_alt=price*amount;return{'status':'success','order_id':order_id_alt,'side':side,'amount':amount,'price':price,'notional':fill_cost_alt}
1373
+ logger.error(f"Kraken REST order failed: {resp.get("error")}");return{'error':resp.get('error','Kraken REST order failed'),'status':'failed'}
1374
+ result=resp.get('result')or{};txids=result.get('txid')or[];order_id=txids[0]if isinstance(txids,list)and txids else txids if isinstance(txids,str)else None;fill_cost=price*amount;logger.info(f"Kraken REST order placed: {side.upper()} {amount:.6f} {base_symbol.upper()}");return{'status':'success','order_id':order_id,'side':side,'amount':amount,'price':price,'notional':fill_cost}
1375
+ order=self.exchange.create_market_order(symbol=routed_symbol,side=side,amount=amount);logger.info(f"Order placed: {side.upper()} {amount:.6f} {base_symbol.upper()}");filled_amount=amount
1376
+ try:
1377
+ order_id=order.get('id');fill_cost=price*filled_amount
1378
+ if order_id:
1379
+ attempts=3
1380
+ for _ in range(attempts):
1381
+ try:
1382
+ fetched=self._with_retries(self.exchange.fetch_order,order_id,symbol=routed_symbol);filled_amount=float(fetched.get('filled',filled_amount)or filled_amount);fill_cost=float(fetched.get('cost',fill_cost)or fill_cost)
1383
+ if fetched.get('status')in['closed','canceled']:break
1384
+ except Exception:pass
1385
+ import time as _t;_t.sleep(max(getattr(self.exchange,'rateLimit',250)/1e3,.25))
1386
+ except Exception:fill_cost=price*filled_amount
1387
+ return{'status':'success','order_id':order.get('id'),'side':side,'amount':filled_amount,'price':price,'notional':fill_cost}
1388
+ except Exception as e:logger.error(f"Trade execution failed: {e}");return{'error':str(e),'status':'failed'}
1389
+ finally:
1390
+ if hasattr(self,'trade_lock')and lock_acquired:
1391
+ try:self.trade_lock.release()
1392
+ except Exception:pass
1393
+ def get_position(self)->Dict[str,Any]:
1394
+ if not self.exchange:return{'error':'Exchange not initialized'}
1395
+ self._update_balance()
1396
+ try:ticker=self.exchange.fetch_ticker('ETH/USD');current_price=float(ticker['last'])
1397
+ except:current_price=.0
1398
+ return{'position':self.position,'available_balance':self.available_balance,'current_price':current_price,'position_value':self.position*current_price}
1399
+ class QuantTradingAgent:
1400
+ def __init__(self,days_back:int=75,demo_mode:bool=False,live_trading_mode:bool=False,exchange_name:str='kraken',api_key:Optional[str]=None,api_secret:Optional[str]=None,api_passphrase:Optional[str]=None,testnet:bool=True,max_position_size:Optional[float]=None,require_confirmation:bool=True,account_size:Optional[float]=None,assets:Optional[List[Dict[str,str]]]=None,longterm_mode:bool=False):
1401
+ load_dotenv();self.longterm_mode=longterm_mode
1402
+ if account_size is not None and account_size<0:logger.warning(f"Negative account_size ({account_size}) provided, using default from TRADING_CONFIG");account_size=None
1403
+ if max_position_size is not None:
1404
+ if max_position_size<0:logger.warning(f"Negative max_position_size ({max_position_size}) provided, using default");max_position_size=None
1405
+ elif max_position_size>1.:logger.warning(f"max_position_size > 1.0 ({max_position_size}) provided, capping at 1.0");max_position_size=1.
1406
+ if longterm_mode:
1407
+ config={**TRADING_CONFIG,**LONGTERM_MODE_CONFIG};self.account_size=account_size or config['account_size'];self.account_category=config.get('account_category',TRADING_CONFIG['account_category']);self.conservative_mode=config['conservative_mode'];self.aggressive_mode=config['aggressive_mode'];self.min_trade_value=config['min_trade_value'];self.position_size_multiplier=config['position_size_multiplier'];self.max_position_size=max_position_size or config['max_position_size'];self.prediction_horizon_days=config['prediction_horizon_days'];self.position_evaluation_interval_hours=config['position_evaluation_interval_hours'];self.min_confidence_threshold=config['min_confidence_threshold'];self.max_asset_price_usd=config['max_asset_price_usd'];self.loop_interval_seconds=config['loop_interval_seconds'];self.full_refresh_interval_hours=config['full_refresh_interval_hours'];hard_cap=config.get('max_position_hard_cap')
1408
+ if hard_cap:self.max_position_size=min(self.max_position_size,hard_cap)
1409
+ else:
1410
+ self.account_size=account_size or TRADING_CONFIG['account_size'];self.account_category=TRADING_CONFIG['account_category'];self.conservative_mode=TRADING_CONFIG['conservative_mode'];self.aggressive_mode=TRADING_CONFIG.get('aggressive_mode',False);self.min_trade_value=TRADING_CONFIG['min_trade_value'];self.position_size_multiplier=TRADING_CONFIG['position_size_multiplier'];self.max_position_size=max_position_size or TRADING_CONFIG.get('max_position_size',.2);hard_cap=TRADING_CONFIG.get('max_position_hard_cap')
1411
+ if hard_cap:self.max_position_size=min(self.max_position_size,hard_cap)
1412
+ self.positions={};self.stop_loss_pct=TRADING_CONFIG.get('stop_loss_pct',-3.);self.stop_gain_pct=TRADING_CONFIG.get('stop_gain_pct',5.);self.initial_portfolio_value=account_size or TRADING_CONFIG['account_size'];self.initial_baseline=self.initial_portfolio_value;self.baseline_value=self.initial_baseline;self.last_portfolio_value=self.initial_portfolio_value;(self.last_full_run):Optional[Dict[str,Any]]=None;(self.last_full_run_time):float=.0;(self.full_refresh_interval):float=3e2;(self.incremental_update_enabled):bool=False;(self.last_full_run):Optional[Dict[str,Any]]=None;(self.last_full_run_time):float=.0;(self.full_refresh_interval):float=3e2;(self.incremental_update_enabled):bool=False;self.promotion_threshold_pct=TRADING_CONFIG.get('promotion_threshold_pct',8.);self.promotion_liquidate_enabled=TRADING_CONFIG.get('promotion_liquidate_enabled',True);self.promotion_debounce_secs=TRADING_CONFIG.get('promotion_debounce_secs',.0);self.promotion_event=False;self.force_halt_after_promotion=False;self.ratchet_armed=False;self.ratchet_promoted=False;(self.cooldown_peak_value):Optional[float]=None;(self.promotion_candidate_value):Optional[float]=None;(self.promotion_candidate_ts):Optional[float]=None;(self.exit_summary):Optional[Dict[str,Any]]=None;self.dynamic_max_position_size=TRADING_CONFIG.get('max_position_size',.2);self.session_pnl_pct=.0;self.circuit_breaker_triggered=False;self.cooldown_enabled=TRADING_CONFIG.get('cooldown_enabled',True);self.system_state='ACTIVE';self.cooldown_loop_trigger=TRADING_CONFIG.get('cooldown_loop_trigger',25);self.cooldown_api_budget_threshold=TRADING_CONFIG.get('cooldown_api_budget_threshold',.3);self.cooldown_congestion_threshold=TRADING_CONFIG.get('cooldown_congestion_threshold',.65);self.cooldown_min_loops=TRADING_CONFIG.get('cooldown_min_loops',3);self.cooldown_max_loops=TRADING_CONFIG.get('cooldown_max_loops',max(self.cooldown_min_loops,4));self.cooldown_sleep_multiplier=TRADING_CONFIG.get('cooldown_sleep_multiplier',1.15);self.cooldown_loops_remaining=0;self.cooldown_total_loops=0;self.cooldown_trigger_reason='';self.loops_since_cooldown=0;self.global_loop_counter=0;self.cooldown_exit_guard=False;self.api_budget_remaining=1.;logger.add('quant_trading_agent.log',rotation='500 MB',retention='10 days',level='INFO',format='{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}');self.days_back=days_back;self.demo_mode=demo_mode and not live_trading_mode;self.live_trading_mode=live_trading_mode;self.assets=assets;self.multi_asset_mode=False;self.auto_multi_asset=AUTO_MULTI_ASSET;self.market_data_client=MarketDataClient(demo_mode=self.demo_mode);self.order_book_tracker=OrderBookTracker();self.socket_manager=ExchangeSocketManager();self.signal_registry=SignalRegistry();self.time_series_signals=TimeSeriesSignals();self.volatility_signals=VolatilitySignals();self.liquidity_signals=LiquiditySignals();self.cross_sectional_signals=CrossSectionalSignals();self.relative_value_signals=RelativeValueSignals();self.regime_signals=RegimeSignals();self.meta_signals=MetaSignals();self.social_sentiment_signals=SocialSentimentSignals();self.onchain_signals=OnChainSignals();self.news_sentiment_signals=NewsSentimentSignals();self.github_signals=GitHubSignals();self.exchange_metrics_signals=ExchangeMetricsSignals();(self.alternative_data_cache):Dict[str,Dict[str,float]]={};(self.alternative_data_timestamps):Dict[str,float]={};self.causal_engine=CausalEngine(longterm_mode=self.longterm_mode);self.signal_validator=SignalValidator(self.causal_engine);self.regime_detector=RegimeDetector();self.causal_ml=CausalMLModels();self.joint_training=JointTrainingEngine(self.causal_engine,self.causal_ml);self.meta_learner=MetaLearner();self.model_factory=ModelFactory();self.target_generator=TargetGenerator();self.ensemble_predictor=EnsemblePredictor();self.cov_estimator=CovarianceEstimator();self.risk_manager=RiskManager()
1413
+ if longterm_mode:rm_cap=self.max_position_size
1414
+ else:
1415
+ rm_cap=max_position_size or TRADING_CONFIG.get('max_position_size',.2);hard_cap=TRADING_CONFIG.get('max_position_hard_cap')
1416
+ if hard_cap:rm_cap=min(rm_cap,hard_cap)
1417
+ self.risk_monitor=RiskMonitor(max_position_size=rm_cap);self.circuit_breaker=CircuitBreaker();self.rotation_manager=AssetRotationManager()if ROTATION_ENABLED else None;self.position_sizer=PositionSizer();self.portfolio_optimizer=PortfolioOptimizer();self.monitoring=MonitoringSystem();self.confidence_calibrator=ConfidenceCalibrator();self.twap_executor=TWAPExecutor();self.vwap_executor=VWAPExecutor();self.smart_router=SmartRouter();self.price_data=pd.DataFrame();self.signals_df=pd.DataFrame();self.current_price=.0;self.streaming_buffer=[];self.streaming_mode=False;(self.price_data_dict):Dict[str,pd.DataFrame]={};(self.signals_df_dict):Dict[str,pd.DataFrame]={};(self.asset_types):Dict[str,str]={}
1418
+ if assets is not None and len(assets)>1:self.multi_asset_mode=True
1419
+ if self.auto_multi_asset and(assets is None or len(assets)<=1):
1420
+ wallet_value=self.account_size;max_asset_price=getattr(self,'max_asset_price_usd',None)if longterm_mode else None;self.asset_discovery=AssetDiscovery(wallet_value=wallet_value,min_asset_value=MIN_ASSET_VALUE,max_assets=MAX_ASSETS_LIMIT,longterm_mode=longterm_mode,max_asset_price_usd=max_asset_price);discovered_assets=self.asset_discovery.discover_assets()
1421
+ if discovered_assets:self.assets=discovered_assets;self.multi_asset_mode=True;logger.info(f"šŸ” Auto-discovered {len(discovered_assets)} assets for trading")
1422
+ if self.multi_asset_mode:
1423
+ assets_to_process=self.assets if self.assets else assets if assets else[]
1424
+ for asset_config in assets_to_process:
1425
+ symbol=asset_config.get('symbol','').upper();asset_type=asset_config.get('type','auto')
1426
+ if symbol:self.asset_types[symbol]=asset_type
1427
+ logger.info(f"Multi-asset mode enabled with {len(assets_to_process)} assets: {list(self.asset_types.keys())}")
1428
+ self.cache_type=ALTERNATIVE_DATA_CONFIG.get('cache_type','redis');self.cache=self._init_cache();self.cache_dir=Path('.cache/signals');self.cache_dir.mkdir(parents=True,exist_ok=True);self._redis_client=None;self.execution_engine=ExecutionEngine(exchange_name=exchange_name,api_key=api_key,api_secret=api_secret,api_passphrase=api_passphrase,testnet=testnet,max_position_size=rm_cap,require_confirmation=require_confirmation,dry_run=not live_trading_mode);self.backtest_engine=BacktestEngine()
1429
+ if self.live_trading_mode:
1430
+ asset_list=assets or ExchangeAssetCatalog.default_assets(exchange_name,MAX_ASSETS_LIMIT);valid_pairs,missing=[],[]
1431
+ for a in asset_list:
1432
+ base=a.get('symbol')if isinstance(a,dict)else None
1433
+ if not base:continue
1434
+ routed=self.execution_engine._derive_symbol(base)if hasattr(self.execution_engine,'_derive_symbol')else None
1435
+ if routed and routed in getattr(self.execution_engine,'markets',{}):valid_pairs.append(routed)
1436
+ else:missing.append(base)
1437
+ if not valid_pairs:raise RuntimeError(f"Live trading enabled but no valid trading symbols found on {exchange_name} for assets {missing or asset_list}")
1438
+ if missing:logger.warning(f"Live trading: missing markets for {missing} on {exchange_name}; tradable pairs: {valid_pairs}")
1439
+ self.alt_data_client=AltDataClient(config=ALTERNATIVE_DATA_CONFIG)
1440
+ def _initialize_live_portfolio_state(self)->None:
1441
+ engine=getattr(self,'execution_engine',None)
1442
+ if engine is None:return
1443
+ if getattr(engine,'exchange_name','').lower()!='kraken':return
1444
+ try:engine._update_balance()
1445
+ except Exception as e:logger.warning(f"Live portfolio init: balance update failed: {e}");return
1446
+ live_value=float(getattr(engine,'available_balance',.0)or .0)
1447
+ if live_value<=0:logger.warning('Live portfolio init: Kraken balance is zero/absent; using configured account_size');return
1448
+ self.initial_portfolio_value=live_value;self.initial_baseline=live_value;self.baseline_value=live_value;self.last_portfolio_value=live_value
1449
+ def fetch_data(self,asset:str='ethereum')->None:
1450
+ try:alt_data=self.alt_data_client.fetch_all_alternative_data(asset,self.days_back);self.alternative_data_cache=alt_data;self.alternative_data_timestamps={k:time.time()for k in alt_data.keys()}
1451
+ except Exception as e:logger.debug(f"Alternative data fetch failed: {e}")
1452
+ if self.multi_asset_mode:
1453
+ self.price_data_dict=self.market_data_client.fetch_multiple_assets(self.assets,self.days_back);self.price_data_dict=self.market_data_client.validate_unified_schema(self.price_data_dict)
1454
+ if(not self.price_data_dict or not any(df is not None and not df.empty for df in self.price_data_dict.values()))and self.live_trading_mode and hasattr(self,'execution_engine')and getattr(self.execution_engine,'exchange',None):
1455
+ import pandas as pd;seeded={}
1456
+ for a in self.assets:
1457
+ sym=a.get('symbol','').upper();routed=self.execution_engine._derive_symbol(sym)if hasattr(self.execution_engine,'_derive_symbol')else None
1458
+ if not routed:continue
1459
+ try:
1460
+ ticker=self.execution_engine.exchange.fetch_ticker(routed);price=ticker.get('last')or ticker.get('close')
1461
+ if price:
1462
+ p=float(price);base_p=self.market_data_client._get_asset_base_price(sym)if hasattr(self,'market_data_client')else None
1463
+ if base_p:
1464
+ lo,hi=base_p*.05,base_p*2e1
1465
+ if p<lo or p>hi:logger.warning(f"Skipping {sym} ticker fallback due to implausible price {p:.2f} (expected ~{base_p:.2f})");continue
1466
+ seeded[sym]=pd.DataFrame({'price':[p*.999,p]})
1467
+ except Exception:continue
1468
+ if seeded:
1469
+ now=pd.Timestamp.utcnow()
1470
+ for(sym,df)in seeded.items():
1471
+ if'date'not in df.columns:df['date']=[now-pd.Timedelta(minutes=1),now]
1472
+ if'returns'not in df.columns:df['returns']=.0
1473
+ df.reset_index(drop=True,inplace=True)
1474
+ self.price_data_dict=seeded
1475
+ if not self.price_data_dict:raise RuntimeError('Failed to fetch multi-asset data (both historical and live fallback)')
1476
+ primary_symbol=list(self.price_data_dict.keys())[0];self.price_data=self.price_data_dict[primary_symbol];self.current_price=self.price_data['price'].iloc[-1]
1477
+ if VERBOSE_LOGGING:logger.debug(f"Fetched multi-asset data: {len(self.price_data_dict)} assets, {len(self.price_data)} points per asset")
1478
+ else:
1479
+ self.price_data=self.market_data_client.fetch_price_data(asset,self.days_back);assert not self.price_data.empty,'Failed to fetch price data';self.current_price=self.price_data['price'].iloc[-1]
1480
+ if VERBOSE_LOGGING:logger.debug(f"Fetched data: {len(self.price_data)} points, current price: ${self.current_price:,.2f}")
1481
+ def compute_signals(self,asset_symbol:Optional[str]=None)->pd.DataFrame:
1482
+ if not isinstance(self.price_data,pd.DataFrame):logger.error(f"price_data is not a DataFrame (type: {type(self.price_data)}), returning empty DataFrame");return pd.DataFrame()
1483
+ symbol_key=asset_symbol or('multi'if self.multi_asset_mode else'single');cache_key=f"signals:{symbol_key}:{hash(str(self.price_data.index[-1])if not self.multi_asset_mode else str(list(self.price_data_dict.keys())))if self.price_data.index.size>0 else"empty"}"
1484
+ if self.cache=='redis'and self._redis_client:
1485
+ try:
1486
+ cached_signals=self._redis_client.get(cache_key)
1487
+ if cached_signals:
1488
+ cached_data=json.loads(cached_signals)
1489
+ if'signals'in cached_data:return pd.DataFrame(cached_data['signals'],index=self.price_data.index if not self.multi_asset_mode else self.price_data_dict[list(self.price_data_dict.keys())[0]].index)
1490
+ except Exception:pass
1491
+ if asset_symbol is not None and self.multi_asset_mode:assert asset_symbol in self.price_data_dict,f"Asset {asset_symbol} not found in price_data_dict";data=self.price_data_dict[asset_symbol].copy()
1492
+ elif self.multi_asset_mode and self.price_data_dict:primary_symbol=list(self.price_data_dict.keys())[0];data=self.price_data_dict[primary_symbol].copy()
1493
+ else:
1494
+ if self.price_data.empty or len(self.price_data)==0:logger.warning('Price data is empty, returning empty signals DataFrame');return pd.DataFrame()
1495
+ if len(self.price_data)==1:logger.warning('Only single row of price data available, duplicating for signal computation');self.price_data=pd.concat([self.price_data,self.price_data],ignore_index=True)
1496
+ data=self.price_data.copy()
1497
+ if'price'not in data.columns:logger.error("Required 'price' column missing from price data");return pd.DataFrame()
1498
+ if'returns'not in data.columns:
1499
+ if len(data)>1:data['returns']=data['price'].pct_change()
1500
+ else:data['returns']=.0
1501
+ static_cache_key=f"static_signals:{symbol_key}";static_signals=None
1502
+ if self.cache=='redis'and self._redis_client:
1503
+ try:
1504
+ cached_static=self._redis_client.get(static_cache_key)
1505
+ if cached_static:static_signals=json.loads(cached_static)
1506
+ except Exception:pass
1507
+ if static_signals is None:
1508
+ static_signals={'momentum':self.time_series_signals.momentum(data).tolist(),'reversal':self.time_series_signals.short_term_reversal(data).tolist(),'sma_dist':self.time_series_signals.sma_distance(data).tolist(),'ema_trend':self.time_series_signals.ema_trend(data).tolist(),'ma_crossover':self.time_series_signals.ma_crossover(data).tolist()}
1509
+ if self.cache=='redis'and self._redis_client:
1510
+ try:self._redis_client.setex(static_cache_key,3600,json.dumps(static_signals))
1511
+ except Exception:pass
1512
+ data['signal_momentum']=pd.Series(static_signals['momentum'],index=data.index);data['signal_reversal']=pd.Series(static_signals['reversal'],index=data.index);data['signal_sma_dist']=pd.Series(static_signals['sma_dist'],index=data.index);data['signal_ema_trend']=pd.Series(static_signals['ema_trend'],index=data.index);data['signal_ma_crossover']=pd.Series(static_signals['ma_crossover'],index=data.index);dynamic_cache_key=f"dynamic_signals:{symbol_key}";dynamic_signals=None
1513
+ if self.cache=='redis'and self._redis_client:
1514
+ try:
1515
+ cached_dynamic=self._redis_client.get(dynamic_cache_key)
1516
+ if cached_dynamic:dynamic_signals=json.loads(cached_dynamic)
1517
+ except Exception:pass
1518
+ if dynamic_signals is None:
1519
+ dynamic_signals={'vol_breakout':self.time_series_signals.volatility_breakout(data).tolist(),'price_level':self.time_series_signals.price_level(data).tolist(),'realized_var':self.volatility_signals.realized_variance(data).tolist(),'realized_vol':self.volatility_signals.realized_volatility(data).tolist(),'garch_vol':self.volatility_signals.garch_volatility(data).tolist(),'vol_of_vol':self.volatility_signals.vol_of_vol(data).tolist(),'skewness':self.volatility_signals.skewness(data).tolist(),'kurtosis':self.volatility_signals.kurtosis(data).tolist(),'vol_clustering':self.volatility_signals.volatility_clustering(data).tolist()}
1520
+ if self.cache=='redis'and self._redis_client:
1521
+ try:self._redis_client.setex(dynamic_cache_key,900,json.dumps(dynamic_signals))
1522
+ except Exception:pass
1523
+ data['signal_vol_breakout']=pd.Series(dynamic_signals['vol_breakout'],index=data.index);data['signal_price_level']=pd.Series(dynamic_signals['price_level'],index=data.index);data['signal_realized_var']=pd.Series(dynamic_signals['realized_var'],index=data.index);data['signal_realized_vol']=pd.Series(dynamic_signals['realized_vol'],index=data.index);data['signal_garch_vol']=pd.Series(dynamic_signals['garch_vol'],index=data.index);data['signal_vol_of_vol']=pd.Series(dynamic_signals['vol_of_vol'],index=data.index);data['signal_skewness']=pd.Series(dynamic_signals['skewness'],index=data.index);data['signal_kurtosis']=pd.Series(dynamic_signals['kurtosis'],index=data.index);data['signal_vol_clustering']=pd.Series(dynamic_signals['vol_clustering'],index=data.index);data['signal_turnover']=self.liquidity_signals.turnover(data);data['signal_volume_zscore']=self.liquidity_signals.volume_zscore(data);data['signal_amihud']=self.liquidity_signals.amihud_illiquidity(data);data['signal_trade_imbalance']=self.liquidity_signals.trade_imbalance(data);data['signal_vpin']=self.liquidity_signals.vpin(data);data['signal_size']=self.cross_sectional_signals.size(data);data['signal_cs_momentum']=self.cross_sectional_signals.cross_sectional_momentum(data);data['signal_low_vol']=self.cross_sectional_signals.low_volatility(data);data['signal_vol_regime']=self.regime_signals.volatility_regime(data);data['signal_liq_regime']=self.regime_signals.liquidity_regime(data)
1524
+ if'returns'in data.columns:
1525
+ market_returns=data['returns'];key_signals=['signal_momentum','signal_realized_vol','signal_amihud']
1526
+ for sig_col in key_signals:
1527
+ if sig_col in data.columns:
1528
+ try:data[f"{sig_col}_crowding"]=self.meta_signals.signal_crowding(data,sig_col,market_returns);data[f"{sig_col}_instability"]=self.meta_signals.signal_instability(data,sig_col)
1529
+ except Exception:pass
1530
+ if self.multi_asset_mode and len(self.price_data_dict)>1:
1531
+ cross_asset_signals=self.compute_cross_asset_signals()
1532
+ if not cross_asset_signals.empty:data=pd.concat([data,cross_asset_signals],axis=1)
1533
+ alternative_signals=self.compute_alternative_signals()
1534
+ if not alternative_signals.empty:data=pd.concat([data,alternative_signals],axis=1)
1535
+ if self.multi_asset_mode:
1536
+ if asset_symbol is not None:self.signals_df_dict[asset_symbol]=data
1537
+ elif self.price_data_dict:primary_symbol=list(self.price_data_dict.keys())[0];self.signals_df_dict[primary_symbol]=data
1538
+ self.signals_df=data
1539
+ if VERBOSE_LOGGING:num_signals=len([c for c in data.columns if c.startswith('signal_')]);asset_info=f" for {asset_symbol}"if asset_symbol else'';logger.debug(f"Computed {num_signals} signals{asset_info}")
1540
+ return data
1541
+ def compute_alternative_signals(self)->pd.DataFrame:
1542
+ if self.price_data.empty or not self.alternative_data_cache:return pd.DataFrame()
1543
+ result=pd.DataFrame(index=self.price_data.index);window_days=ALTERNATIVE_DATA_CONFIG.get('window_size_days',7)
1544
+ try:
1545
+ onchain_data=self.alternative_data_cache.get('onchain',{})
1546
+ if onchain_data:result['signal_onchain_addr']=onchain_data.get('active_addresses',.0);result['signal_onchain_tx']=onchain_data.get('transaction_volume',.0);result['signal_onchain_growth']=onchain_data.get('network_growth',.0);result['signal_onchain_health']=self.onchain_signals.network_health_score(onchain_data)
1547
+ social_data=self.alternative_data_cache.get('social',{})
1548
+ if social_data:result['signal_twitter']=social_data.get('twitter_sentiment',.0);result['signal_reddit']=social_data.get('reddit_sentiment',.0);result['signal_social_momentum']=self.social_sentiment_signals.social_momentum(social_data)
1549
+ news_data=self.alternative_data_cache.get('news',{})
1550
+ if news_data:result['signal_news']=news_data.get('news_sentiment',.0);result['signal_headline']=news_data.get('headline_sentiment',.0);result['signal_news_volume']=news_data.get('news_volume',.0)
1551
+ github_data=self.alternative_data_cache.get('github',{})
1552
+ if github_data:result['signal_github']=self.github_signals.github_activity(github_data);result['signal_github_momentum']=self.github_signals.github_momentum(github_data);result['signal_github_health']=self.github_signals.github_community_health(github_data)
1553
+ exchange_data=self.alternative_data_cache.get('exchange',{})
1554
+ if exchange_data:result['signal_funding']=self.exchange_metrics_signals.funding_rate(exchange_data);result['signal_oi']=self.exchange_metrics_signals.open_interest(exchange_data);result['signal_ls_ratio']=self.exchange_metrics_signals.long_short_ratio(exchange_data)
1555
+ except Exception as e:logger.debug(f"Alternative signal computation failed: {e}")
1556
+ if self.cache=='redis'and self._redis_client:
1557
+ try:cache_data={'signals':result.to_dict('index')if isinstance(result,pd.DataFrame)else result,'timestamp':time.time()};cache_key=f"signals:{self.exchange_name}:{self.symbol}"if hasattr(self,'symbol')else'signals:default';self._redis_client.setex(cache_key,1800,json.dumps(cache_data))
1558
+ except Exception:pass
1559
+ return result
1560
+ def compute_cross_asset_signals(self)->pd.DataFrame:
1561
+ if not self.multi_asset_mode or len(self.price_data_dict)<2:return pd.DataFrame()
1562
+ primary_symbol=list(self.price_data_dict.keys())[0];primary_data=self.price_data_dict[primary_symbol];asset_symbols=list(self.price_data_dict.keys());signal_series={};AlignSignal=lambda sig:sig.reindex(primary_data.index,fill_value=.0).ffill().fillna(.0)if not sig.empty else pd.Series(index=primary_data.index,dtype=float)
1563
+ for(i,asset1)in enumerate(asset_symbols):
1564
+ for asset2 in asset_symbols[i+1:]:
1565
+ data1,data2=self.price_data_dict[asset1],self.price_data_dict[asset2]
1566
+ if'date'not in data1.columns or'date'not in data2.columns:logger.debug(f"Skipping cross-asset signals for {asset1}-{asset2}: missing date column");continue
1567
+ corr_signal=self.relative_value_signals.cross_asset_correlation(data1,data2)
1568
+ if not corr_signal.empty:signal_series[f"signal_{asset1}_{asset2}_corr"]=AlignSignal(corr_signal)
1569
+ rs_signal=self.relative_value_signals.relative_strength(data1,data2)
1570
+ if not rs_signal.empty:signal_series[f"signal_{asset1}_{asset2}_rs"]=AlignSignal(rs_signal)
1571
+ if asset1.upper()=='BTC'and asset2.upper()=='ETH'or asset1.upper()=='ETH'and asset2.upper()=='BTC':
1572
+ btc_data=self.price_data_dict.get('BTC')if'BTC'in self.price_data_dict else self.price_data_dict.get('btc')if'btc'in self.price_data_dict else None;eth_data=self.price_data_dict.get('ETH')if'ETH'in self.price_data_dict else self.price_data_dict.get('eth')if'eth'in self.price_data_dict else None
1573
+ if btc_data is not None and eth_data is not None and not btc_data.empty and not eth_data.empty:
1574
+ btc_eth_corr=self.relative_value_signals.btc_eth_correlation(eth_data,btc_data)
1575
+ if not btc_eth_corr.empty:signal_series['signal_btc_eth_corr']=AlignSignal(btc_eth_corr)
1576
+ result=pd.concat(signal_series,axis=1)if signal_series else pd.DataFrame(index=primary_data.index)
1577
+ if VERBOSE_LOGGING:logger.debug(f"Computed {len(result.columns)} cross-asset signals")
1578
+ if self.cache=='redis'and self._redis_client:
1579
+ try:cache_data={'signals':result.to_dict('index')if isinstance(result,pd.DataFrame)else result,'timestamp':time.time()};cache_key=f"cross_signals:{self.exchange_name}"if hasattr(self,'exchange_name')else'cross_signals:default';self._redis_client.setex(cache_key,1800,json.dumps(cache_data))
1580
+ except Exception:pass
1581
+ return result
1582
+ def build_causal_graph(self)->None:
1583
+ variables=['price','volume','volatility','returns','momentum','market_sentiment','trading_volume','liquidity'];edges=[('volume','price'),('volatility','price'),('momentum','price'),('market_sentiment','momentum'),('market_sentiment','trading_volume'),('trading_volume','price'),('liquidity','price')]
1584
+ if self.alternative_data_cache:
1585
+ if'onchain'in self.alternative_data_cache:variables.extend(['onchain_metrics','network_growth']);edges.extend([('onchain_metrics','volume'),('network_growth','price')])
1586
+ if'social'in self.alternative_data_cache:variables.append('social_sentiment');edges.extend([('social_sentiment','market_sentiment'),('social_sentiment','momentum')])
1587
+ if'news'in self.alternative_data_cache:variables.append('news_sentiment');edges.extend([('news_sentiment','market_sentiment'),('news_sentiment','volatility')])
1588
+ if'exchange'in self.alternative_data_cache:variables.append('exchange_metrics');edges.append(('exchange_metrics','liquidity'))
1589
+ self.causal_engine.build_scm(variables,edges)
1590
+ def _compute_alternative_signal_confidence(self,signal_name:str,data_source:str)->float:
1591
+ weights=ALTERNATIVE_DATA_CONFIG.get('confidence_weights',{'freshness':.4,'reliability':.4,'stability':.2});freshness=1.
1592
+ if data_source in self.alternative_data_timestamps:age_seconds=time.time()-self.alternative_data_timestamps[data_source];ttl=ALTERNATIVE_DATA_CONFIG['cache_ttl'].get(data_source,3600);freshness=max(.0,1.-age_seconds/ttl)
1593
+ reliability=.8 if data_source in self.alternative_data_cache else .0;stability=.7;confidence=weights['freshness']*freshness+weights['reliability']*reliability+weights['stability']*stability;return float(np.clip(confidence,.0,1.))
1594
+ def validate_signals(self)->Dict[str,Dict[str,float]]:
1595
+ if self.signals_df.empty or'returns'not in self.signals_df.columns:return{}
1596
+ self.build_causal_graph();df_vars=['price','volume','volatility','returns','momentum'];available_vars=[v for v in df_vars if v in self.signals_df.columns]
1597
+ if len(available_vars)>=3:
1598
+ try:self.causal_engine.fit_from_data(self.signals_df,available_vars,window=30)
1599
+ except Exception as e:logger.debug(f"CRCA fit failed: {e}")
1600
+ target=self.signals_df['returns'].shift(-1);regimes=self.signals_df.get('signal_vol_regime',pd.Series());signal_scores={};signal_cols=[c for c in self.signals_df.columns if c.startswith('signal_')and not c.endswith('_crowding')and not c.endswith('_instability')]
1601
+ for signal_col in signal_cols[:20]:
1602
+ signal_values=self.signals_df[signal_col];aligned=pd.concat([signal_values,target],axis=1).dropna()
1603
+ if len(aligned)>10:
1604
+ score=self.signal_validator.compute_causal_score(signal_col,aligned.iloc[:,0],aligned.iloc[:,1],regimes if not regimes.empty else None)
1605
+ if any(alt_prefix in signal_col for alt_prefix in['twitter','reddit','onchain','news','github','funding','oi']):
1606
+ data_source='social'if any(x in signal_col for x in['twitter','reddit'])else'onchain'if'onchain'in signal_col else'news'if'news'in signal_col or'headline'in signal_col else'github'if'github'in signal_col else'exchange'if any(x in signal_col for x in['funding','oi','ls'])else'unknown'
1607
+ if data_source!='unknown':alt_confidence=self._compute_alternative_signal_confidence(signal_col,data_source);score['score']=score.get('score',.0)*alt_confidence;score['alternative_confidence']=alt_confidence
1608
+ signal_scores[signal_col]=score
1609
+ if VERBOSE_LOGGING:logger.debug(f"Validated {len(signal_scores)} signals")
1610
+ return signal_scores
1611
+ def generate_predictions(self,signal_scores:Dict[str,Dict[str,float]])->Tuple[np.ndarray,np.ndarray,Dict[str,Any]]:
1612
+ if self.multi_asset_mode:
1613
+ primary_symbol=list(self.price_data_dict.keys())[0]if self.price_data_dict else None
1614
+ if primary_symbol:
1615
+ multi_preds=self.generate_multi_asset_predictions(signal_scores)
1616
+ if primary_symbol in multi_preds:pred,intervals,metadata=multi_preds[primary_symbol];return pred,intervals,metadata
1617
+ if self.signals_df.empty:return np.array([]),np.array([]),{}
1618
+ available_signals=set(self.signals_df.columns);valid_signals=[s for(s,score)in signal_scores.items()if s in available_signals and score.get('score',0)>.5]
1619
+ if not valid_signals:valid_signals=[c for c in self.signals_df.columns if c.startswith('signal_')]
1620
+ valid_signals=[s for s in valid_signals if s in available_signals]
1621
+ if not valid_signals:logger.warning('No valid signals found in signals_df, returning zero predictions');return np.array([.0]*len(self.signals_df)),np.array([[.01]]),{}
1622
+ X=self.signals_df[valid_signals].fillna(0).values
1623
+ if self.longterm_mode and hasattr(self,'prediction_horizon_days'):k_periods=max(1,int(self.prediction_horizon_days));y=self.target_generator.generate_forward_returns(self.signals_df,k=k_periods).fillna(0).values
1624
+ else:y=self.target_generator.generate_forward_returns(self.signals_df,k=1).fillna(0).values
1625
+ train_size=int(len(X)*.8)
1626
+ if train_size<10:return np.zeros(len(X)),np.array([[.01]]),{}
1627
+ X_train,X_test=X[:train_size],X[train_size:];y_train,y_test=y[:train_size],y[train_size:];models,model_predictions,trained_models={},{},[]
1628
+ if SKLEARN_AVAILABLE:
1629
+ model_configs=[('linear',{'type':'linear'}),('rf',{'type':'rf','n_estimators':50}),('gb',{'type':'gb','n_estimators':50})]
1630
+ for(name,config)in model_configs:
1631
+ try:
1632
+ model=self.model_factory.create_model(config['type'],**{k:v for(k,v)in config.items()if k!='type'})
1633
+ if model is not None:model.fit(X_train,y_train);models[name]=model;trained_models.append(name);self.ensemble_predictor.add_model(name,model,weight=1.);model_predictions[name]=model.predict(X)
1634
+ except Exception as e:
1635
+ if VERBOSE_LOGGING:logger.debug(f"Model {name} training failed: {e}")
1636
+ ensemble_weights={}
1637
+ if len(trained_models)>0:
1638
+ pred_array=np.array([model_predictions[name]for name in trained_models]);weights=np.ones(len(trained_models))/len(trained_models)
1639
+ if hasattr(self,'causal_engine'):
1640
+ try:
1641
+ weights=self.meta_learner.optimize(signal_names=trained_models,recent_performance={m:1. for m in trained_models},regime='normal',model_names=trained_models)[0].values();weights=np.array(list(weights))
1642
+ if np.sum(weights)>0:weights=weights/np.sum(weights)
1643
+ except Exception:weights=np.ones(len(trained_models))/len(trained_models)
1644
+ ensemble_weights={m:float(w)for(m,w)in zip(trained_models,weights)};predictions=np.average(pred_array,axis=0,weights=weights);full_pred_std=np.maximum(np.std(pred_array,axis=0),np.abs(predictions)*.05)
1645
+ else:predictions=np.zeros(len(X));full_pred_std=np.zeros(len(X));logger.warning('No models could be trained - returning zero predictions')
1646
+ signal_contributions={}
1647
+ if SKLEARN_AVAILABLE and'rf'in models and models['rf']is not None:
1648
+ try:
1649
+ rf_model=models['rf'];feature_importance=rf_model.feature_importances_
1650
+ for(i,sig_name)in enumerate(valid_signals):
1651
+ if i<len(feature_importance):signal_contributions[sig_name]=float(feature_importance[i])
1652
+ except Exception:pass
1653
+ if'returns'in self.signals_df.columns:
1654
+ returns_df=pd.DataFrame({'returns':self.signals_df['returns']});covariance=self.cov_estimator.ewma_covariance(returns_df)
1655
+ if CovarianceEstimator.CovSize(covariance)==0:covariance=np.array([[.01]])
1656
+ else:covariance=np.array([[.01]])
1657
+ avg_signal_score=.5
1658
+ if signal_scores:
1659
+ signal_score_values=[v.get('score',.0)for v in signal_scores.values()if isinstance(v,dict)]
1660
+ if len(signal_score_values)>0:avg_signal_score=float(np.mean(signal_score_values))
1661
+ per_model_pred={m:float(model_predictions[m][-1])for m in trained_models}if trained_models else{};per_model_std={m:float(np.std(model_predictions[m]))for m in trained_models}if trained_models else{};prediction_metadata={'signal_contributions':signal_contributions,'prediction_std':full_pred_std.tolist()if len(full_pred_std)>0 else[],'valid_signals':valid_signals,'model_count':len(trained_models),'model_names':trained_models,'ensemble_weights':ensemble_weights,'per_model_pred':per_model_pred,'per_model_std':per_model_std,'avg_signal_score':avg_signal_score,'is_ensemble':len(trained_models)>1}
1662
+ if VERBOSE_LOGGING:
1663
+ if len(trained_models)>1:logger.debug(f"Ensemble prediction using {len(trained_models)} models: {", ".join(trained_models)} weights={ensemble_weights}")
1664
+ else:logger.debug(f"Single model prediction using: {trained_models[0]if trained_models else"none"}")
1665
+ return predictions,covariance,prediction_metadata
1666
+ def generate_multi_asset_predictions(self,signal_scores:Dict[str,Dict[str,float]])->Dict[str,Tuple[np.ndarray,Dict[str,np.ndarray],Dict[str,Any]]]:
1667
+ if not self.multi_asset_mode or not self.price_data_dict:return{}
1668
+ results={}
1669
+ for(symbol,asset_data)in self.price_data_dict.items():
1670
+ try:
1671
+ assert symbol in self.price_data_dict and'returns'in asset_data.columns,f"Invalid asset data for {symbol}";asset_signals=[c for c in self.signals_df.columns if c.startswith(f"{symbol}_signal_")or c.startswith('signal_')and not any(a in c for a in self.price_data_dict.keys()if a!=symbol)];cross_asset_signals=[c for c in self.signals_df.columns if f"_{symbol}_"in c or f"{symbol}_"in c];all_signals=list(set(asset_signals+cross_asset_signals))
1672
+ if not all_signals:all_signals=[c for c in self.signals_df.columns if c.startswith('signal_')]
1673
+ available_signals=set(self.signals_df.columns);valid_signals=[s for s in all_signals if s in available_signals and s in signal_scores and signal_scores[s].get('score',0)>.5]
1674
+ if not valid_signals:valid_signals=[s for s in all_signals if s in available_signals][:20]
1675
+ if not valid_signals:logger.warning(f"No valid signals found for {symbol}, skipping prediction");continue
1676
+ dfX=self.signals_df[valid_signals].infer_objects(copy=False);X=np.nan_to_num(dfX.to_numpy(dtype=float,copy=True),nan=.0,posinf=.0,neginf=.0)
1677
+ if self.longterm_mode and hasattr(self,'prediction_horizon_days'):k_periods=max(1,int(self.prediction_horizon_days));y_raw=self.target_generator.generate_forward_returns(asset_data,k=k_periods,target_col='price').infer_objects(copy=False)
1678
+ else:y_raw=self.target_generator.generate_forward_returns(asset_data,k=1,target_col='price').infer_objects(copy=False)
1679
+ y=np.nan_to_num(y_raw.to_numpy(dtype=float,copy=True),nan=.0,posinf=.0,neginf=.0);min_len=min(len(X),len(y));X,y=X[:min_len],y[:min_len];assert len(X)>=10,f"Insufficient data for {symbol}";train_size=int(len(X)*.8);assert train_size>=10,f"Insufficient training data for {symbol}";X_train,X_test=X[:train_size],X[train_size:];y_train,y_test=y[:train_size],y[train_size:];models,model_predictions={},{}
1680
+ if SKLEARN_AVAILABLE:models['linear']=self.model_factory.create_model('linear');models['rf']=self.model_factory.create_model('rf',n_estimators=50)
1681
+ for(name,model)in models.items():
1682
+ assert model is not None,f"Model {name} is None"
1683
+ try:model.fit(X_train,y_train);model_predictions[name]=model.predict(X_test)
1684
+ except Exception:pass
1685
+ assert model_predictions,f"No model predictions for {symbol}";pred_array=np.array(list(model_predictions.values()));weights=np.ones(len(model_predictions))/len(model_predictions)
1686
+ if hasattr(self,'causal_engine'):
1687
+ try:
1688
+ weights=self.meta_learner.optimize(signal_names=list(model_predictions.keys()),recent_performance={m:1. for m in model_predictions.keys()},regime='normal',model_names=list(model_predictions.keys()))[0].values();weights=np.array(list(weights))
1689
+ if np.sum(weights)>0:weights=weights/np.sum(weights)
1690
+ except Exception:weights=np.ones(len(model_predictions))/len(model_predictions)
1691
+ ensemble_weights={m:float(w)for(m,w)in zip(model_predictions.keys(),weights)};predictions=np.average(pred_array,axis=0,weights=weights);pred_std=np.maximum(np.std(pred_array,axis=0),np.abs(predictions)*.05);full_predictions=np.zeros(len(X));full_pred_std=np.zeros(len(X));full_predictions[train_size:]=predictions;full_pred_std[train_size:]=pred_std
1692
+ if train_size>0:full_predictions[:train_size]=np.mean(predictions)if len(predictions)>0 else .0;full_pred_std[:train_size]=np.mean(pred_std)if len(pred_std)>0 else abs(full_predictions[:train_size])*.1
1693
+ latest_pred=full_predictions[-1]if len(full_predictions)>0 else .0;latest_std=full_pred_std[-1]if len(full_pred_std)>0 else abs(latest_pred)*.1;current_price=asset_data['price'].iloc[-1];n_simulations=250
1694
+ if self.longterm_mode and hasattr(self,'prediction_horizon_days'):base_horizon=self.prediction_horizon_days;horizon_days=[base_horizon,base_horizon*2,base_horizon*4]
1695
+ else:horizon_days=[1,3,7]
1696
+ pred_return=latest_pred;pred_std_return=latest_std
1697
+ if abs(pred_return)>1.:pred_return=pred_return/current_price if current_price>0 else .0;pred_std_return=pred_std_return/current_price if current_price>0 else abs(pred_return)*.1
1698
+ intervals={}
1699
+ for horizon in horizon_days:
1700
+ simulated_prices=[]
1701
+ for _ in range(n_simulations):daily_returns=np.random.normal(pred_return,pred_std_return,horizon);final_price=current_price*np.exp(np.sum(daily_returns));simulated_prices.append(final_price)
1702
+ simulated_prices=np.array(simulated_prices);mean_price=float(np.mean(simulated_prices));lower_price=float(np.percentile(simulated_prices,2.5));upper_price=float(np.percentile(simulated_prices,97.5));intervals[f"{horizon}d"]={'lower':lower_price,'upper':upper_price,'mean':mean_price,'median':float(np.median(simulated_prices)),'std':float(np.std(simulated_prices))}
1703
+ metadata={'num_signals':len(valid_signals),'prediction_uncertainty':float(np.mean(full_pred_std)),'prediction_std':full_pred_std.tolist(),'asset':symbol,'model_count':len(model_predictions),'model_names':list(model_predictions.keys()),'ensemble_weights':ensemble_weights,'per_model_pred':{m:float(pred_array[i][-1])for(i,m)in enumerate(model_predictions.keys())},'per_model_std':{m:float(np.std(pred_array[i]))for(i,m)in enumerate(model_predictions.keys())}};results[symbol]=full_predictions,intervals,metadata
1704
+ if VERBOSE_LOGGING:logger.debug(f"[MULTI] {symbol}: models={list(model_predictions.keys())} weights={ensemble_weights}")
1705
+ except Exception as e:logger.debug(f"Failed to generate predictions for {symbol}: {e}");continue
1706
+ if VERBOSE_LOGGING:logger.debug(f"Generated predictions for {len(results)} assets")
1707
+ return results
1708
+ def optimize_portfolio(self,expected_returns:Union[Dict[str,float],np.ndarray],covariance:Union[pd.DataFrame,np.ndarray,Dict[str,Any]],signal_scores:Optional[Dict[str,Dict[str,float]]]=None)->Union[Dict[str,float],np.ndarray]:
1709
+ if self.multi_asset_mode:
1710
+ if isinstance(expected_returns,dict)and isinstance(covariance,pd.DataFrame):
1711
+ asset_types_list=[self.asset_types.get(symbol,'crypto')for symbol in expected_returns.keys()];cross_asset_constraints={}
1712
+ for asset_type in set(asset_types_list):cross_asset_constraints[asset_type]=.6 if asset_type=='crypto'else .4 if asset_type=='stock'else .2 if asset_type=='fx'else .3
1713
+ return self.portfolio_optimizer.optimize_asset_allocation(expected_returns_dict=expected_returns,covariance_df=covariance,constraints={'asset_types':asset_types_list,'cross_asset_constraints':cross_asset_constraints,'max_leverage':1.})
1714
+ if len(expected_returns)==0:return np.array([])
1715
+ cov_array=np.array(covariance.values)if isinstance(covariance,pd.DataFrame)else covariance if isinstance(covariance,np.ndarray)else np.array(covariance)
1716
+ if cov_array.size==0:return np.array([])
1717
+ n=len(expected_returns)
1718
+ if cov_array.shape!=(n,n):cov_array=np.eye(n)*.01
1719
+ return self.portfolio_optimizer.optimize_cvar(expected_returns=expected_returns,covariance=cov_array,max_leverage=1.)
1720
+ def _compute_confidence_only(self,predictions:np.ndarray,signal_scores:Dict[str,Dict[str,float]],pred_metadata:Dict[str,Any],confidence_intervals:Dict[str,Dict[str,float]])->Tuple[float,List[str]]:
1721
+ try:
1722
+ weight=float(predictions[0])if len(predictions)>0 else .0;pred_std=pred_metadata.get('prediction_std',[]);latest_pred=predictions[-1]if len(predictions)>0 else .0;confidence_factors,explanations=[],[];signal_strength=abs(weight)
1723
+ if np.isnan(signal_strength)or signal_strength<1e-06:signal_strength=.0
1724
+ if signal_scores:
1725
+ for(signal_name,signal_data)in signal_scores.items():
1726
+ if isinstance(signal_data,dict)and'score'in signal_data:
1727
+ score_val=float(signal_data['score'])
1728
+ if not np.isnan(score_val):confidence_factors.append(abs(score_val)*.8)
1729
+ if len(signal_scores)>0:
1730
+ avg_signal_score=np.mean([v.get('score',.0)for v in signal_scores.values()if isinstance(v,dict)])
1731
+ if not np.isnan(avg_signal_score):confidence_factors.append(avg_signal_score*.9)
1732
+ heuristic_confidence=min(1.,max(.0,sum(confidence_factors)));statistical_confidence=(lambda ci_1d:(lambda ci_width,ci_mean:.9 if(relative_width:=ci_width/abs(ci_mean)if abs(ci_mean)>1e-06 else 1.)<.1 else .75 if relative_width<.2 else .6 if relative_width<.3 else .45 if relative_width<.5 else .3)(ci_1d['upper']-ci_1d['lower'],ci_1d['mean'])if'lower'in ci_1d and'upper'in ci_1d and'mean'in ci_1d and ci_1d['mean']>0 else .5)(confidence_intervals.get('1d',{}))if'1d'in confidence_intervals else .5;ensemble_confidence=(lambda latest_std:.85 if(cv:=abs(latest_std)/abs(latest_pred))<.2 else .7 if cv<.3 else .55 if cv<.5 else .4)(pred_std[-1])if len(pred_std)>0 and abs(latest_pred)>1e-06 else .5;raw_confidence=min(1.,max(.0,.4*heuristic_confidence+.35*statistical_confidence+.25*ensemble_confidence));confidence=self.confidence_calibrator.calibrate_prob(raw_confidence)if hasattr(self,'confidence_calibrator')and self.confidence_calibrator else raw_confidence;confidence=min(1.,max(.0,confidence));causal_score,causal_block=self._evaluate_causal_stability()
1733
+ if causal_block:confidence=.0;explanations.append(f" Causal block: unstable causal structure (score {causal_score:.2f})")
1734
+ else:multiplier=max(.5,min(1.,causal_score));confidence*=multiplier;explanations.append(f"Causal stability: score {causal_score:.2f}, confidence scaled by {multiplier:.2f}")
1735
+ if VERBOSE_LOGGING:calib_info=f" (calibrated: {confidence:.0%})"if hasattr(self,'confidence_calibrator')and self.confidence_calibrator.isotonic_model else'';explanations.append(f"Confidence breakdown: heuristic={heuristic_confidence:.0%}, statistical={statistical_confidence:.0%}, ensemble={ensemble_confidence:.0%}, raw={raw_confidence:.0%}{calib_info}")
1736
+ return confidence,explanations
1737
+ except Exception as e:logger.warning(f"Confidence calculation failed: {e}, using default");return .5,[f"Confidence calculation failed: {str(e)}"]
1738
+ def _get_scm_dataset(self,window:int=200,horizon:int=1)->Optional[pd.DataFrame]:
1739
+ try:
1740
+ df=None
1741
+ if hasattr(self,'signals_df')and self.signals_df is not None and not self.signals_df.empty:df=self.signals_df.copy()
1742
+ elif hasattr(self,'price_data')and self.price_data is not None and not self.price_data.empty:df=self.price_data.copy()
1743
+ if df is None or df.empty:return
1744
+ work=df.copy().tail(window+horizon+5)
1745
+ if'returns'not in work.columns and'price'in work.columns:work['returns']=work['price'].pct_change()
1746
+ work['future_return']=work['returns'].shift(-horizon);work['momentum']=work['returns'].shift(1);work['trend']=work['returns'].rolling(window=5,min_periods=3).mean().shift(1)if'returns'in work.columns else np.nan;work['reversion_pressure']=((work['returns']-work['returns'].rolling(window=10,min_periods=5).mean())/(work['returns'].rolling(window=10,min_periods=5).std()+1e-06)).shift(1)if'returns'in work.columns else np.nan;work['short_vol']=work['returns'].rolling(window=5,min_periods=3).std().shift(1)if'returns'in work.columns else np.nan;work['regime_vol']=work['returns'].ewm(span=20,adjust=False).std().shift(1)if'returns'in work.columns else np.nan
1747
+ if'volume'in work.columns:work['volume']=work['volume'].shift(1)
1748
+ elif'turnover'in work.columns:work['volume']=work['turnover'].shift(1)
1749
+ else:work['volume']=np.nan
1750
+ if'liquidity'in work.columns:work['liquidity']=work['liquidity'].shift(1)
1751
+ else:work['liquidity']=np.nan
1752
+ if'sentiment'in work.columns:work['sentiment']=work['sentiment'].shift(1)
1753
+ else:work['sentiment']=np.nan
1754
+ if'funding_regime'in work.columns:work['funding_regime']=work['funding_regime'].shift(1)
1755
+ if'session_time'in work.columns:work['session_time']=work['session_time'].shift(1)
1756
+ cols=['sentiment','liquidity','regime_vol','volume','momentum','trend','short_vol','reversion_pressure','future_return']
1757
+ if'funding_regime'in work.columns:cols.append('funding_regime')
1758
+ if'session_time'in work.columns:cols.append('session_time')
1759
+ work=work[cols].dropna()
1760
+ if len(work)<50:return
1761
+ return work.tail(window)
1762
+ except Exception:return
1763
+ def _fit_causal_graph(self,dataset:pd.DataFrame)->Dict[str,Dict[str,float]]:
1764
+ edges={'volume':['sentiment','liquidity'],'momentum':['sentiment','liquidity','volume'],'short_vol':['regime_vol'],'future_return':['momentum','trend','reversion_pressure','short_vol']};opt_edges={'short_vol':['liquidity'],'momentum':['short_vol']};strengths:Dict[str,Dict[str,float]]={}
1765
+ for(child,parents)in edges.items():
1766
+ strengths[child]={}
1767
+ for p in parents:
1768
+ if p in dataset.columns and child in dataset.columns:strengths[child][p]=.0
1769
+ for(child,parents)in opt_edges.items():
1770
+ if child not in strengths:strengths[child]={}
1771
+ for p in parents:
1772
+ if p in dataset.columns and child in dataset.columns:strengths[child][p]=.0
1773
+ try:
1774
+ for(child,parents)in strengths.items():
1775
+ y=dataset[child].values;X_cols=[p for p in parents.keys()]
1776
+ if not X_cols:continue
1777
+ X=dataset[X_cols].values
1778
+ if X.shape[0]<X.shape[1]+5:continue
1779
+ coef,_,_,_=np.linalg.lstsq(X,y,rcond=None)
1780
+ for(i,p)in enumerate(X_cols):strengths[child][p]=float(coef[i])
1781
+ except Exception as e:
1782
+ if VERBOSE_LOGGING:logger.debug(f"SCM fit failed: {e}")
1783
+ return strengths
1784
+ def _simulate_interventions(self,strengths:Dict[str,Dict[str,float]],dataset:pd.DataFrame,n_scenarios:int=50,epsilon:float=.1)->Dict[str,Any]:
1785
+ if'future_return'not in dataset.columns:return{'edge_effects':{},'score':1.}
1786
+ target_std=float(dataset['future_return'].std()or 1e-06);parents=['sentiment','liquidity','volume','momentum','short_vol','trend','reversion_pressure'];edge_effects:Dict[str,List[float]]={p:[]for p in parents}
1787
+ for _ in range(n_scenarios):
1788
+ row=dataset.sample(1).iloc[0];base_return=float(row['future_return'])
1789
+ for p in parents:
1790
+ if p not in row or p not in dataset.columns:continue
1791
+ perturbed=row.copy();delta=epsilon*(1 if np.random.rand()>.5 else-1);perturbed[p]=perturbed[p]+delta;mom=perturbed['momentum']
1792
+ if p in strengths.get('momentum',{}):mom+=strengths['momentum'][p]*delta
1793
+ fut=perturbed['future_return'];fut+=strengths.get('future_return',{}).get('momentum',.0)*(mom-row['momentum']);fut+=strengths.get('future_return',{}).get('trend',.0)*(perturbed.get('trend',row.get('trend',.0))-row.get('trend',.0));fut+=strengths.get('future_return',{}).get('reversion_pressure',.0)*(perturbed.get('reversion_pressure',row.get('reversion_pressure',.0))-row.get('reversion_pressure',.0));fut+=strengths.get('future_return',{}).get('short_vol',.0)*(perturbed.get('short_vol',row.get('short_vol',.0))-row.get('short_vol',.0));edge_effects[p].append((fut-base_return)/(target_std+1e-06))
1794
+ edge_scores=[]
1795
+ for(p,effects)in edge_effects.items():
1796
+ if not effects:continue
1797
+ effects=np.array(effects);mean_eff=np.mean(effects);sign_flip_penalty=1. if np.sign(np.median(effects))==np.sign(np.mean(effects))else .5;magnitude=min(1.,max(.0,abs(mean_eff)));edge_scores.append(magnitude*sign_flip_penalty)
1798
+ score=float(np.mean(edge_scores))if edge_scores else 1.;return{'edge_effects':edge_effects,'score':score}
1799
+ def _compute_causal_score(self,strengths:Dict[str,Dict[str,float]],dataset:pd.DataFrame)->float:
1800
+ if'future_return'not in dataset.columns or dataset['future_return'].std()==0:return 1.
1801
+ target_std=float(dataset['future_return'].std());edge_scores=[]
1802
+ def norm_score(coef:float)->float:s=abs(coef)/(target_std+1e-06);return float(max(.0,min(1.,s)))
1803
+ for parent in['momentum','trend','reversion_pressure','short_vol']:coef=strengths.get('future_return',{}).get(parent,.0);edge_scores.append(norm_score(coef))
1804
+ vol_mom=strengths.get('momentum',{}).get('volume',.0);mom_fr=strengths.get('future_return',{}).get('momentum',.0);chain_score=norm_score(vol_mom*mom_fr);edge_scores.append(chain_score);sv_mom=strengths.get('momentum',{}).get('short_vol',.0);chain_sv=norm_score(sv_mom*mom_fr);edge_scores.append(chain_sv)
1805
+ if not edge_scores:return 1.
1806
+ return float(np.mean(edge_scores))
1807
+ def _validate_with_crca(self,signal:str,expected_return:float,confidence:float,symbol:str)->Dict[str,Any]:
1808
+ if not hasattr(self,'causal_engine')or not self.causal_engine:return{'approved':True,'reason':'CRCAAgent not available'}
1809
+ try:
1810
+ crca=self.causal_engine.crca
1811
+ if signal=='BUY':
1812
+ if expected_return<=0:return{'approved':False,'reason':'Expected return not positive'}
1813
+ if confidence<getattr(self,'min_confidence_threshold',.85):return{'approved':False,'reason':f"Confidence {confidence:.0%} below threshold {getattr(self,"min_confidence_threshold",.85):.0%}"}
1814
+ return{'approved':True,'reason':'CRCAAgent validation passed'}
1815
+ elif signal=='SELL':
1816
+ if expected_return>=0:return{'approved':False,'reason':'Expected return not negative'}
1817
+ if confidence<getattr(self,'min_confidence_threshold',.85):return{'approved':False,'reason':f"Confidence {confidence:.0%} below threshold {getattr(self,"min_confidence_threshold",.85):.0%}"}
1818
+ return{'approved':True,'reason':'CRCAAgent validation passed'}
1819
+ else:return{'approved':True,'reason':'HOLD signal, no validation needed'}
1820
+ except Exception as e:
1821
+ logger.warning(f"CRCAAgent validation error: {e}")
1822
+ if self.longterm_mode:return{'approved':False,'reason':f"Validation error: {str(e)}"}
1823
+ return{'approved':True,'reason':'Validation error, proceeding with caution'}
1824
+ def _evaluate_causal_stability(self)->Tuple[float,bool]:
1825
+ neutral_score,neutral_block=1.,False;dataset=self._get_scm_dataset()
1826
+ if dataset is None:return neutral_score,neutral_block
1827
+ strengths=self._fit_causal_graph(dataset);structural_score=self._compute_causal_score(strengths,dataset);mc=self._simulate_interventions(strengths,dataset,n_scenarios=50,epsilon=.1);score=float(np.mean([structural_score,mc.get('score',1.)]))
1828
+ if getattr(self,'conservative_mode',False):block_threshold=.45
1829
+ elif getattr(self,'aggressive_mode',False):block_threshold=.25
1830
+ else:block_threshold=.35
1831
+ causal_block=score<block_threshold
1832
+ if VERBOSE_LOGGING:logger.debug(f"[SCM] causal_score={score:.3f}, block={causal_block}, threshold={block_threshold}, structural={structural_score:.3f}, mc={mc.get("score",1.):.3f}")
1833
+ return score,causal_block
1834
+ def _make_trading_decision(self,portfolio_weights:np.ndarray,predictions:np.ndarray,signal_scores:Dict[str,Dict[str,float]],pred_metadata:Dict[str,Any],confidence_intervals:Dict[str,Dict[str,float]],volatility:float,current_price:float,max_position_size:Optional[float]=None,symbol:str=None)->Dict[str,Any]:
1835
+ explanations=[]
1836
+ if max_position_size is None:max_position_size=getattr(self,'risk_monitor',None);max_position_size=max_position_size.max_position_size if max_position_size else TRADING_CONFIG['max_position_size']
1837
+ assert len(portfolio_weights)>0 and len(predictions)>0,'Insufficient data for decision';weight,latest_pred=portfolio_weights[0],predictions[-1]
1838
+ if weight is None or not isinstance(weight,(int,float))or np.isnan(weight)or np.isinf(weight):weight=.0;explanations.append('āš ļø Invalid portfolio weight, using 0.0')
1839
+ confidence_factors=[];signal_strength=abs(weight)
1840
+ if np.isnan(signal_strength)or signal_strength<1e-06:
1841
+ signal_strength=.0
1842
+ if VERBOSE_LOGGING:logger.debug(f"Signal strength is zero or NaN, weight={weight}")
1843
+ if signal_strength>.2:confidence_factors.append(.3);explanations.append(f"Strong signal strength ({signal_strength:.2%})")
1844
+ elif signal_strength>.1:confidence_factors.append(.2);explanations.append(f"Moderate signal strength ({signal_strength:.2%})")
1845
+ elif signal_strength>.05:confidence_factors.append(.1);explanations.append(f"Weak signal strength ({signal_strength:.2%})")
1846
+ else:confidence_factors.append(.05);explanations.append(f"Very weak signal strength ({signal_strength:.2%})")
1847
+ model_count,model_names=pred_metadata.get('model_count',1),pred_metadata.get('model_names',[])
1848
+ if model_count>=3:confidence_factors.append(.2);explanations.append(f"Ensemble prediction: {", ".join(model_names[:3])if model_names else f"{model_count} models"} agree")
1849
+ elif model_count>=2:confidence_factors.append(.15);explanations.append(f"Multiple models ({", ".join(model_names)if model_names else f"{model_count} models"})")
1850
+ else:confidence_factors.append(.1);explanations.append(f"Single model prediction ({model_names[0]if model_names else"single model"})")
1851
+ base_signals=[]
1852
+ if symbol:base_signals=[(k,v.get('score',.0))for(k,v)in signal_scores.items()if symbol in k or k.startswith('signal_')]
1853
+ if not base_signals:base_signals=[(k,v.get('score',.0))for(k,v)in signal_scores.items()]
1854
+ TopSignals=lambda n:sorted(base_signals,key=lambda x:(-x[1],x[0]))[:n];AvgSignalScore=lambda sigs:np.mean([s[1]for s in sigs])if sigs else .0;top_signals=TopSignals(5);avg_signal_score=AvgSignalScore(top_signals);signal_contributions=pred_metadata.get('signal_contributions',{})
1855
+ if avg_signal_score>.7:confidence_factors.append(.25);explanations.append(f"High-quality signals (avg score: {avg_signal_score:.2f})");top_contributors=[f"{sig_name.replace("signal_","")[:15]} ({signal_contributions.get(sig_name,.0):.1%})"if signal_contributions.get(sig_name,.0)>0 else f"{sig_name.replace("signal_","")[:15]} (score: {sig_score:.2f})"for(sig_name,sig_score)in top_signals[:3]];explanations.append(f"Top signals: {", ".join(top_contributors)if top_contributors else", ".join([s[0].replace("signal_","")[:15]for s in top_signals[:3]])}")
1856
+ elif avg_signal_score>.5:
1857
+ confidence_factors.append(.15);explanations.append(f"Moderate signal quality (avg score: {avg_signal_score:.2f})")
1858
+ if top_signals:explanations.append(f"Best signal: {top_signals[0][0].replace("signal_","")[:15]} (score: {top_signals[0][1]:.2f})")
1859
+ else:
1860
+ confidence_factors.append(.1);explanations.append(f"Low signal quality (avg score: {avg_signal_score:.2f})")
1861
+ if top_signals:explanations.append(f"Best signal: {top_signals[0][0].replace("signal_","")[:15]} (score: {top_signals[0][1]:.2f})")
1862
+ pred_std=pred_metadata.get('prediction_std',[]);latest_std=pred_std[-1]if len(pred_std)>0 else abs(latest_pred)*.1;is_prediction_in_returns=abs(latest_pred)<1. and current_price>1e1
1863
+ if is_prediction_in_returns:uncertainty_ratio=abs(latest_std)/abs(latest_pred)if abs(latest_pred)>1e-06 else abs(latest_std)*current_price/current_price if current_price>0 else 1.
1864
+ elif current_price>0:pred_return=latest_pred/current_price;std_return=latest_std/current_price if latest_std>0 else abs(pred_return)*.1;uncertainty_ratio=abs(std_return)/abs(pred_return)if abs(pred_return)>1e-06 else abs(std_return)if abs(std_return)>0 else 1.
1865
+ else:uncertainty_ratio=1.
1866
+ if uncertainty_ratio>1e1:logger.warning(f"!!! Suspiciously high uncertainty ratio: {uncertainty_ratio:.2f} (pred={latest_pred:.6f}, std={latest_std:.6f}, price={current_price:.2f}). Using fallback calculation.");fallback_uncertainty=min(2.,max(.1,abs(latest_std)/max(abs(latest_pred),current_price*.01)));uncertainty_ratio=fallback_uncertainty
1867
+ uncertainty_ratio=max(.01,min(uncertainty_ratio,2.))
1868
+ if uncertainty_ratio<.3:confidence_factors.append(.15);explanations.append(f"Low prediction uncertainty ({uncertainty_ratio:.2%})")
1869
+ elif uncertainty_ratio<.5:confidence_factors.append(.1);explanations.append(f"Moderate prediction uncertainty ({uncertainty_ratio:.2%})")
1870
+ elif uncertainty_ratio<1.:confidence_factors.append(.05);explanations.append(f"High prediction uncertainty ({uncertainty_ratio:.2%}) - reduces confidence")
1871
+ else:confidence_factors.append(.0);explanations.append(f"{"!!! Extremely"if uncertainty_ratio>1.5 else"Very"} high prediction uncertainty ({uncertainty_ratio:.2%}) - {"very "if uncertainty_ratio>1.5 else""}low directional confidence")
1872
+ if volatility<.2:confidence_factors.append(.1);explanations.append(f"Low market volatility ({volatility:.2%})")
1873
+ elif volatility<.4:confidence_factors.append(.05);explanations.append(f"Moderate volatility ({volatility:.2%})")
1874
+ else:confidence_factors.append(.0);explanations.append(f"High volatility ({volatility:.2%}) - increased risk")
1875
+ heuristic_confidence=min(1.,max(.0,sum(confidence_factors)));statistical_confidence=(lambda ci_1d:(lambda ci_width,ci_mean:.9 if(relative_width:=ci_width/abs(ci_mean)if abs(ci_mean)>1e-06 else 1.)<.1 else .75 if relative_width<.2 else .6 if relative_width<.3 else .45 if relative_width<.5 else .3)(ci_1d['upper']-ci_1d['lower'],ci_1d['mean'])if'lower'in ci_1d and'upper'in ci_1d and'mean'in ci_1d and ci_1d['mean']>0 else .5)(confidence_intervals.get('1d',{}))if'1d'in confidence_intervals else .5;ensemble_confidence=(lambda latest_std:.85 if(cv:=abs(latest_std)/abs(latest_pred))<.2 else .7 if cv<.3 else .55 if cv<.5 else .4)(pred_std[-1])if len(pred_std)>0 and abs(latest_pred)>1e-06 else .5;raw_confidence=min(1.,max(.0,.4*heuristic_confidence+.35*statistical_confidence+.25*ensemble_confidence));confidence=self.confidence_calibrator.calibrate_prob(raw_confidence)if hasattr(self,'confidence_calibrator')and self.confidence_calibrator else raw_confidence;confidence=min(1.,max(.0,confidence))
1876
+ if VERBOSE_LOGGING:
1877
+ calib_info=f" (calibrated: {confidence:.0%})"if hasattr(self,'confidence_calibrator')and self.confidence_calibrator.isotonic_model else'';explanations.append(f"Confidence breakdown: heuristic={heuristic_confidence:.0%}, statistical={statistical_confidence:.0%}, ensemble={ensemble_confidence:.0%}, raw={raw_confidence:.0%}{calib_info}")
1878
+ if hasattr(self,'confidence_calibrator'):
1879
+ calib_metrics=self.confidence_calibrator.get_calibration_metrics()
1880
+ if calib_metrics['calibration_samples']>0:explanations.append(f"Calibration: Brier score={calib_metrics["brier_score"]:.3f}, samples={calib_metrics["calibration_samples"]}")
1881
+ conservative_mode=getattr(self,'conservative_mode',False);aggressive_mode=getattr(self,'aggressive_mode',False)
1882
+ if conservative_mode:base_threshold=.7
1883
+ elif aggressive_mode:base_threshold=.35
1884
+ else:base_threshold=.55
1885
+ if self.longterm_mode and hasattr(self,'min_confidence_threshold'):base_threshold=self.min_confidence_threshold;explanations.append(f"šŸ” Longterm mode: Using strict confidence threshold of {base_threshold:.0%}")
1886
+ min_confidence_threshold,block_trade_due_to_uncertainty=base_threshold,False
1887
+ if conservative_mode:extreme_uncertainty=2.;very_high_uncertainty=1.5;high_uncertainty=1.2;moderate_uncertainty=.8
1888
+ elif aggressive_mode:extreme_uncertainty=5.;very_high_uncertainty=4.;high_uncertainty=3.;moderate_uncertainty=2.
1889
+ else:extreme_uncertainty=3.;very_high_uncertainty=2.;high_uncertainty=1.5;moderate_uncertainty=1.
1890
+ if uncertainty_ratio>extreme_uncertainty:block_trade_due_to_uncertainty=True;explanations.append(f"!!! Trade blocked: Extremely high prediction uncertainty ({uncertainty_ratio:.2%})")
1891
+ elif uncertainty_ratio>very_high_uncertainty:min_confidence_threshold=max(.8,base_threshold+.15);explanations.append(f"!!! Very high uncertainty ({uncertainty_ratio:.2%}) - requiring {min_confidence_threshold:.0%} confidence")
1892
+ elif uncertainty_ratio>high_uncertainty:min_confidence_threshold=max(.75,base_threshold+.1);explanations.append(f"!!! High uncertainty ({uncertainty_ratio:.2%}) - requiring {min_confidence_threshold:.0%} confidence")
1893
+ elif uncertainty_ratio>moderate_uncertainty:min_confidence_threshold=max(.7,base_threshold+.05);explanations.append(f"!!! Moderate-high uncertainty ({uncertainty_ratio:.2%}) - requiring {min_confidence_threshold:.0%} confidence")
1894
+ elif uncertainty_ratio>.7:min_confidence_threshold=base_threshold+.03
1895
+ avg_signal_score=pred_metadata.get('avg_signal_score',.5)
1896
+ if conservative_mode:min_signal_quality=.35
1897
+ elif aggressive_mode:min_signal_quality=.1
1898
+ else:min_signal_quality=.2
1899
+ causal_score,causal_block=self._evaluate_causal_stability()
1900
+ if causal_block:explanations.insert(0,f"!!! HOLD: Causal block (score {causal_score:.2f})");return{'signal':'HOLD','confidence':confidence,'explanations':explanations,'recommended_position_size':.0,'top_signals':top_signals,'avg_signal_score':avg_signal_score,'expected_return':expected_return,'volatility':volatility,'causal_score':causal_score,'causal_block':causal_block}
1901
+ else:damp=max(.5,min(1.,causal_score));confidence*=damp;avg_signal_score*=damp
1902
+ if VERBOSE_LOGGING:
1903
+ explanations.append(f"DEBUG: signal_strength={signal_strength:.3f}, confidence={confidence:.1%}, threshold={min_confidence_threshold:.1%}, avg_signal_score={avg_signal_score:.2f}, uncertainty={uncertainty_ratio:.2f}, block_trade={block_trade_due_to_uncertainty}, expected_return={expected_return:.4f}")
1904
+ if signal_strength>.15 and signal_direction>0:explanations.append(f"STRONG BUY criteria: strength>{.15} āœ“, confidence>{min_confidence_threshold} {"āœ“"if confidence>min_confidence_threshold else"āœ—"}, signal_score>={min_signal_quality} {"āœ“"if avg_signal_score>=min_signal_quality else"āœ—"}")
1905
+ elif signal_strength>.1 and signal_direction>0:explanations.append(f"MODERATE BUY criteria: strength>{.1} āœ“, confidence>{min_confidence_threshold} {"āœ“"if confidence>min_confidence_threshold else"āœ—"}, signal_score>={min_signal_quality} {"āœ“"if avg_signal_score>=min_signal_quality else"āœ—"}")
1906
+ elif signal_strength>.15 and signal_direction<0:explanations.append(f"STRONG SELL criteria: strength>{.15} āœ“, confidence>{min_confidence_threshold} {"āœ“"if confidence>min_confidence_threshold else"āœ—"}, signal_score>={min_signal_quality} {"āœ“"if avg_signal_score>=min_signal_quality else"āœ—"}")
1907
+ elif signal_strength>.1 and signal_direction<0:explanations.append(f"MODERATE SELL criteria: strength<{.1} āœ“, confidence>{min_confidence_threshold} {"āœ“"if confidence>min_confidence_threshold else"āœ—"}, signal_score>={min_signal_quality} {"āœ“"if avg_signal_score>=min_signal_quality else"āœ—"}")
1908
+ else:explanations.append(f"HOLD: signal too weak ({signal_strength:.3f} strength, direction={signal_direction})")
1909
+ if uncertainty_ratio>1.:logger.debug(f"High uncertainty ({uncertainty_ratio:.2%}) requires confidence >{min_confidence_threshold:.0%}, current: {confidence:.0%}")
1910
+ signal,entry_level,stop_loss,take_profit='HOLD',current_price,None,None
1911
+ if abs(latest_pred)<=1.5:raw_expected_return=latest_pred
1912
+ else:raw_expected_return=(latest_pred-current_price)/current_price if current_price>0 else .0
1913
+ pred_std=pred_metadata.get('prediction_std',[]);latest_std=pred_std[-1]if len(pred_std)>0 else abs(raw_expected_return)*.1;reliability_ratio=abs(latest_std)/max(abs(raw_expected_return),1e-06);prediction_reliable=True;expected_return=raw_expected_return
1914
+ if abs(raw_expected_return)<1e-06:
1915
+ prediction_reliable=False;expected_return=.0
1916
+ if VERBOSE_LOGGING:explanations.append('DEBUG: raw_expected_return ~0, treating as neutral')
1917
+ elif reliability_ratio>5. or abs(raw_expected_return)>2.:
1918
+ prediction_reliable=False;expected_return=.0
1919
+ if VERBOSE_LOGGING:explanations.append(f"DEBUG: unreliable prediction (ratio={reliability_ratio:.1f}, ret={raw_expected_return:.1%}) -> neutral")
1920
+ elif abs(raw_expected_return)>1.:
1921
+ expected_return=raw_expected_return*.5
1922
+ if VERBOSE_LOGGING:explanations.append(f"DEBUG: large prediction {raw_expected_return:.1%} softened to {expected_return:.1%}")
1923
+ if'1d'in confidence_intervals and isinstance(confidence_intervals['1d'],dict):
1924
+ ci_mean=confidence_intervals['1d'].get('mean',None)
1925
+ if ci_mean is not None and current_price>0:target_ret=(ci_mean-current_price)/current_price;expected_return=.6*expected_return+.4*target_ret
1926
+ expected_return=max(-.5,min(.5,expected_return))
1927
+ if not prediction_reliable:
1928
+ confidence=confidence*.7
1929
+ if VERBOSE_LOGGING:explanations.append(f"DEBUG: Prediction unreliable, confidence -> {confidence:.1%}")
1930
+ if VERBOSE_LOGGING and prediction_reliable:explanations.append(f"DEBUG: latest_pred={latest_pred:.6f}, current_price={current_price:.2f}, expected_return={expected_return:.2%} (reliable)")
1931
+ if conservative_mode:strong_threshold=.12;moderate_threshold=.08
1932
+ elif aggressive_mode:strong_threshold=.03;moderate_threshold=.015
1933
+ else:strong_threshold=.06;moderate_threshold=.03
1934
+ signal_strength=confidence*.4+avg_signal_score*.4+min(abs(expected_return),.1)*10*.2;signal_direction=1 if expected_return>0 else-1
1935
+ if VERBOSE_LOGGING:explanations.append(f"DEBUG: signal_strength={signal_strength:.3f}, expected_return={expected_return:.4f}, signal_direction={signal_direction}")
1936
+ if not block_trade_due_to_uncertainty and signal_strength>strong_threshold and confidence>min_confidence_threshold and avg_signal_score>=min_signal_quality and signal_direction>0:signal='BUY';stop_loss=min(current_price*.98,confidence_intervals['1d']['lower']);take_profit=max(current_price*1.03,confidence_intervals['1d']['upper']);explanations.insert(0,f"BUY signal: Strong bullish momentum (strength: {signal_strength:.2f}, {confidence:.0%} confidence, expected: {expected_return:.2%})")
1937
+ elif not block_trade_due_to_uncertainty and signal_strength>moderate_threshold and confidence>min_confidence_threshold and avg_signal_score>=min_signal_quality and signal_direction>0:signal='BUY';stop_loss=current_price*.98;take_profit=confidence_intervals['1d']['upper'];explanations.insert(0,f"BUY signal: Moderate bullish signal (strength: {signal_strength:.2f}, confidence: {confidence:.0%}, expected return: {expected_return:.2%})")
1938
+ elif not block_trade_due_to_uncertainty and signal_strength>strong_threshold and confidence>min_confidence_threshold and avg_signal_score>=min_signal_quality and signal_direction<0:signal='SELL';stop_loss=max(current_price*1.02,confidence_intervals['1d']['upper']);take_profit=min(current_price*.97,confidence_intervals['1d']['lower']);explanations.insert(0,f"SELL signal: Strong bearish momentum (strength: {signal_strength:.2f}, {confidence:.0%} confidence, expected: {expected_return:.2%})")
1939
+ elif not block_trade_due_to_uncertainty and signal_strength>moderate_threshold and confidence>min_confidence_threshold and avg_signal_score>=min_signal_quality and signal_direction<0:signal='SELL';stop_loss=current_price*1.02;take_profit=confidence_intervals['1d']['lower'];explanations.insert(0,f"SELL signal: Moderate bearish signal (strength: {signal_strength:.2f}, confidence: {confidence:.0%}, expected return: {expected_return:.2%})")
1940
+ elif not block_trade_due_to_uncertainty and expected_return>.01 and confidence>=min_confidence_threshold-.05:signal='BUY';stop_loss=current_price*.985;take_profit=current_price*(1+max(expected_return,.02));explanations.insert(0,f"BUY signal (fallback): expected {expected_return:.2%}, confidence {confidence:.0%}")
1941
+ elif not block_trade_due_to_uncertainty and expected_return<-.01 and confidence>=min_confidence_threshold-.05:signal='SELL';stop_loss=current_price*1.015;take_profit=current_price*(1+min(expected_return,-.02));explanations.insert(0,f"SELL signal (fallback): expected {expected_return:.2%}, confidence {confidence:.0%}")
1942
+ else:
1943
+ hold_reasons=[]
1944
+ if signal_strength<=moderate_threshold:hold_reasons.append(f"signal too weak ({signal_strength:.3f} strength, need >{moderate_threshold:.3f})")
1945
+ if confidence<=min_confidence_threshold:hold_reasons.append(f"low confidence ({confidence:.0%}, required: {min_confidence_threshold:.0%})")
1946
+ if avg_signal_score<min_signal_quality:hold_reasons.append(f"insufficient signal quality ({avg_signal_score:.2f}, required: {min_signal_quality:.2f})")
1947
+ if block_trade_due_to_uncertainty:hold_reasons.append(f"extremely high uncertainty ({uncertainty_ratio:.2%})")
1948
+ explanations.insert(0,f"HOLD: {", ".join(hold_reasons)if hold_reasons else f"Signal too weak ({signal_strength:.3f} strength) or low confidence ({confidence:.0%}, required: {min_confidence_threshold:.0%})"}")
1949
+ if uncertainty_ratio>1. and confidence<=min_confidence_threshold:explanations.append(f"!!! High uncertainty ({uncertainty_ratio:.2%}) requires {min_confidence_threshold:.0%} confidence, but only {confidence:.0%} achieved")
1950
+ if signal!='HOLD':
1951
+ if self.longterm_mode and hasattr(self,'causal_engine')and self.causal_engine:
1952
+ try:
1953
+ causal_validation=self._validate_with_crca(signal,expected_return,confidence,symbol or'MAIN')
1954
+ if not causal_validation.get('approved',False):signal='HOLD';explanations.insert(0,f"šŸ” Longterm mode: CRCAAgent validation failed - {causal_validation.get("reason","Causal analysis does not support this trade")}");logger.info(f"Longterm mode: Trade blocked by CRCAAgent validation for {symbol or"MAIN"}")
1955
+ except Exception as e:
1956
+ logger.warning(f"CRCAAgent validation failed: {e}, proceeding with caution")
1957
+ if self.longterm_mode:explanations.append(f"!!! CRCAAgent validation error, using extra caution")
1958
+ try:
1959
+ position_sizing_method=getattr(self,'position_sizing_method','kelly')
1960
+ if position_sizing_method not in['kelly','risk_parity','target_vol']:position_sizing_method='kelly';explanations.append('!!! Invalid position sizing method, using Kelly')
1961
+ if volatility is None or isinstance(volatility,float)and(np.isnan(volatility)or np.isinf(volatility)):volatility=.02;explanations.append('!!! Volatility data unavailable, using default 2%')
1962
+ elif volatility<=0:volatility=.02;explanations.append('!!! Invalid volatility value, using default 2%')
1963
+ else:
1964
+ if volatility>2.:volatility/=1e2;explanations.append('! High volatility normalized from percent scale')
1965
+ if volatility>.8:volatility=.8;explanations.append('!!! Extremely high volatility capped at 80%')
1966
+ if uncertainty_ratio is None or isinstance(uncertainty_ratio,float)and(np.isnan(uncertainty_ratio)or np.isinf(uncertainty_ratio)):uncertainty_ratio=1.;explanations.append('āš ļø Uncertainty ratio invalid, using default 100%')
1967
+ if latest_pred is None or isinstance(latest_pred,float)and(np.isnan(latest_pred)or np.isinf(latest_pred)):latest_pred=expected_return*current_price if expected_return is not None and current_price>0 else .0
1968
+ if current_price is None or current_price<=0:current_price=1.
1969
+ er=latest_pred/current_price if abs(latest_pred)>1. and current_price>0 else latest_pred
1970
+ if er is None or not isinstance(er,(int,float))or np.isnan(er)or np.isinf(er):er=.0;explanations.append('!!! Invalid expected return for position sizing, using 0%')
1971
+ if volatility is None or not isinstance(volatility,(int,float))or np.isnan(volatility)or np.isinf(volatility)or volatility<=0:volatility=.02;explanations.append('!!! Invalid volatility for position sizing, using 2%')
1972
+ if uncertainty_ratio is None or not isinstance(uncertainty_ratio,(int,float))or np.isnan(uncertainty_ratio)or np.isinf(uncertainty_ratio):uncertainty_ratio=1.;explanations.append('āš ļø Invalid uncertainty ratio for position sizing, using 100%')
1973
+ if confidence is None or not isinstance(confidence,(int,float))or np.isnan(confidence)or np.isinf(confidence):confidence=.5;explanations.append('!!! Invalid confidence for position sizing, using 50%')
1974
+ if max_position_size is None or not isinstance(max_position_size,(int,float))or np.isnan(max_position_size)or max_position_size<=0:max_position_size=.1;explanations.append('!!! Invalid max position size, using 10%')
1975
+ base_size=min((abs(er/(volatility*volatility))*.25 if position_sizing_method=='kelly'else .1/volatility if position_sizing_method=='risk_parity'else getattr(self,'target_volatility',.15)/volatility if position_sizing_method=='target_vol'else abs(er)/(getattr(self,'risk_aversion',2.)*volatility*volatility))if volatility>1e-06 else abs(weight)*.1,max_position_size);recommended_size=max(.01,min(max_position_size,(lambda vol_adj:vol_adj*(.7 if volatility>.4 else .85 if volatility>.3 else 1.)*(.3 if(unc_ratio_sizing:=min(uncertainty_ratio,2.))>1.5 else .4 if unc_ratio_sizing>1. else .6 if unc_ratio_sizing>.7 else .75 if unc_ratio_sizing>.5 else 1.)*getattr(self,'position_size_multiplier',1.))(base_size*confidence*(1-min(volatility,.5)))))
1976
+ if self.longterm_mode:recommended_size=min(recommended_size,getattr(self,'max_position_size',.005));explanations.append(f"šŸ” Longterm mode: Position size capped at {recommended_size:.2%}")
1977
+ if aggressive_mode and not self.longterm_mode:recommended_size=min(max_position_size,recommended_size*1.5)
1978
+ uncertainty_ratio=uncertainty_ratio if isinstance(uncertainty_ratio,(int,float))and not np.isnan(uncertainty_ratio)else 1.
1979
+ if min(uncertainty_ratio,2.)>1.5:explanations.append(f"!!! Extreme uncertainty: Position size reduced by 70%")
1980
+ if hasattr(self,'risk_monitor')and self.risk_monitor:
1981
+ account_size=getattr(self,'account_size',TRADING_CONFIG['account_size']);portfolio_value=account_size;stop_loss_distance=abs((stop_loss-entry_level)/entry_level)if stop_loss and entry_level>0 else .02;current_positions={k:v.get('size',.0)/portfolio_value if portfolio_value>0 else .0 for(k,v)in self.positions.items()}if hasattr(self,'positions')else{};is_valid,reason,adjusted_size=self.risk_monitor.pre_trade_check(signal=signal,position_size=recommended_size,current_positions=current_positions,portfolio_value=portfolio_value,stop_loss_distance=stop_loss_distance,asset_volatility=volatility,asset_type=getattr(self,'asset_types',{}).get(symbol,'crypto'))
1982
+ if not is_valid:
1983
+ if VERBOSE_LOGGING:explanations.append(f"!!! Risk limit: {reason}")
1984
+ recommended_size=adjusted_size
1985
+ if recommended_size<.001:recommended_size,signal=.0,'HOLD';explanations.append('Trade cancelled: Position size too small after risk adjustments')
1986
+ account_size=getattr(self,'account_size',TRADING_CONFIG['account_size']);trade_value=recommended_size*account_size;daily_volume=float(self.price_data['volume'].iloc[-20:].mean()*current_price)if hasattr(self,'price_data')and not self.price_data.empty and'volume'in self.price_data.columns and current_price>0 else None
1987
+ if hasattr(self,'transaction_cost_model'):
1988
+ transaction_costs=self.transaction_cost_model.estimate_total_cost(trade_size=recommended_size*account_size/current_price if current_price>0 else .0,trade_value=trade_value,current_price=current_price,daily_volume=daily_volume);expected_return_net=expected_return-transaction_costs['total_pct']
1989
+ if expected_return_net<=0 and not aggressive_mode:explanations.append(f"āš ļø Trade cancelled: Transaction costs ({transaction_costs["total_pct"]:.2%}) exceed expected return ({expected_return:.2%})");recommended_size,signal=.0,'HOLD'
1990
+ elif VERBOSE_LOGGING:explanations.append(f"Transaction costs: {transaction_costs["total_pct"]:.2%} (spread: {transaction_costs["spread"]/trade_value:.2%}, slippage: {transaction_costs["slippage"]/trade_value:.2%}, impact: {transaction_costs["impact"]/trade_value:.2%}, fee: {transaction_costs["fee"]/trade_value:.2%})");explanations.append(f"Net expected return: {expected_return_net:.2%} (gross: {expected_return:.2%})")
1991
+ if signal!='HOLD'and current_price>0 and account_size>0:
1992
+ min_trade_value=getattr(self,'min_trade_value',TRADING_CONFIG['min_trade_value']);account_fraction_for_min=min_trade_value/account_size
1993
+ if account_fraction_for_min<=max_position_size and recommended_size<account_fraction_for_min:recommended_size=account_fraction_for_min;explanations.append(f"Adjusted to meet minimum trade value ({min_trade_value:.2f} EUR)")
1994
+ except Exception as sizing_error:explanations.append(f"āš ļø Position sizing calculation failed: {str(sizing_error)}");recommended_size=.0
1995
+ else:recommended_size=.0
1996
+ return{'signal':signal,'confidence':confidence,'explanations':explanations,'recommended_position_size':recommended_size,'entry_level':entry_level,'stop_loss':stop_loss,'take_profit':take_profit,'expected_return':expected_return,'prediction_uncertainty':float(uncertainty_ratio),'top_signals':top_signals,'volatility':volatility,'current_price':current_price,'causal_score':causal_score,'causal_block':causal_block}
1997
+ def _make_multi_asset_decisions(self,portfolio_weights:Dict[str,float],predictions:Dict[str,np.ndarray],signal_scores:Dict[str,Dict[str,float]],pred_metadata_dict:Dict[str,Dict[str,Any]],confidence_intervals_dict:Dict[str,Dict[str,Dict[str,float]]],volatility_dict:Dict[str,float],current_prices:Dict[str,float],max_position_size:Optional[float]=None)->Dict[str,Dict[str,Any]]:
1998
+ decisions={}
1999
+ for symbol in portfolio_weights.keys():
2000
+ if symbol not in predictions or symbol not in current_prices:continue
2001
+ volatility=volatility_dict.get(symbol,.02);volatility=.02 if volatility is None or isinstance(volatility,float)and np.isnan(volatility)else volatility;decision=self._make_trading_decision(portfolio_weights=np.array([portfolio_weights.get(symbol,.0)]),predictions=predictions[symbol],signal_scores=signal_scores.get(symbol,{}),pred_metadata=pred_metadata_dict.get(symbol,{}),confidence_intervals=confidence_intervals_dict.get(symbol,{}),volatility=volatility,current_price=current_prices[symbol],max_position_size=max_position_size,symbol=symbol);decisions[symbol]=decision
2002
+ return decisions
2003
+ def backtest(self,start_date:datetime,end_date:datetime,train_window:int=60,test_window:int=7,step:int=7)->Dict[str,Any]:results=self.backtest_engine.run_backtest(agent=self,start_date=start_date,end_date=end_date,train_window=train_window,test_window=test_window,step=step);report=self.backtest_engine._generate_report(results);logger.info(f"\n{report}");return results
2004
+ async def start_streaming(self,exchange:str='coinbase',symbol:str='ETH',decision_callback:Optional[Callable]=None):
2005
+ self.streaming_mode,self.streaming_buffer,self.streaming_order_book,self.last_signal_update,self.signal_update_interval,self.decision_callback=True,[],None,0,5,decision_callback
2006
+ async def handle_stream_data(data:Dict[str,Any]):
2007
+ try:
2008
+ msg_type,price,volume=data.get('type',''),None,.0
2009
+ if msg_type=='ticker':price,volume=data.get('price',0),data.get('volume_24h',0)
2010
+ elif msg_type=='trade':price,volume=data.get('price',0),data.get('size',0)
2011
+ elif msg_type=='orderbook':
2012
+ self.streaming_order_book=data.get('data');order_book=self.streaming_order_book
2013
+ if order_book and'bids'in order_book and'asks'in order_book:
2014
+ bids,asks=sorted([float(p)for p in order_book['bids'].keys()],reverse=True),sorted([float(p)for p in order_book['asks'].keys()])
2015
+ if bids and asks:price=(bids[0]+asks[0])/2
2016
+ else:price,volume=data.get('price')or data.get('last_price')or data.get('c'),data.get('volume',0)or data.get('v',0)
2017
+ if price and price>0:
2018
+ self.streaming_buffer.append({'timestamp':datetime.now(),'price':float(price),'volume':float(volume)})
2019
+ if len(self.streaming_buffer)>2000:self.streaming_buffer=self.streaming_buffer[-2000:]
2020
+ self.current_price=float(price);current_time=time.time()
2021
+ if current_time-self.last_signal_update>=self.signal_update_interval:
2022
+ await self._compute_streaming_signals();self.last_signal_update=current_time
2023
+ if self.decision_callback:
2024
+ try:await self.decision_callback(self)
2025
+ except Exception as e:logger.debug(f"Error in decision callback: {e}")
2026
+ except Exception as e:logger.debug(f"Error handling stream data: {e}")
2027
+ success=await self.socket_manager.connect(exchange,symbol,handle_stream_data)
2028
+ if success:logger.info(f"Started streaming mode for {exchange}:{symbol}")
2029
+ else:logger.error(f"Failed to start streaming for {exchange}:{symbol}");self.streaming_mode=False
2030
+ async def _compute_streaming_signals(self):
2031
+ if len(self.streaming_buffer)<30:return
2032
+ try:
2033
+ stream_df=pd.DataFrame(self.streaming_buffer);stream_df.set_index('timestamp',inplace=True)
2034
+ if'price'not in stream_df.columns:return
2035
+ stream_df['returns']=stream_df['price'].pct_change()
2036
+ if'volume'not in stream_df.columns:stream_df['volume']=.0
2037
+ self.price_data=stream_df[['price','volume']].copy();signals_df=self.compute_signals()
2038
+ if not signals_df.empty:self.signals_df=signals_df;logger.debug(f"Updated signals from streaming data: {len(signals_df)} points, latest price: ${self.current_price:.2f}")
2039
+ except Exception as e:logger.debug(f"Error computing streaming signals: {e}")
2040
+ async def stop_streaming(self,exchange:str='coinbase',symbol:str='ETH'):self.streaming_mode=False;await self.socket_manager.disconnect(exchange,symbol);logger.info('Stopped streaming mode')
2041
+ async def run_batched_multi_asset(self)->Dict[str,Any]:
2042
+ if not self.multi_asset_mode or not self.assets:return await self.run()
2043
+ self.global_loop_counter+=1
2044
+ if self.system_state!='COOLDOWN':
2045
+ self.loops_since_cooldown+=1
2046
+ if self.cooldown_exit_guard:self.cooldown_exit_guard=False
2047
+ api_budget_remaining=getattr(self,'api_budget_remaining',1.)
2048
+ if self.system_state=='COOLDOWN':self._cooldown_tick(api_budget_remaining)
2049
+ portfolio_metrics=self._update_session_pnl();self._maybe_trigger_session_circuit_breaker(portfolio_metrics);num_assets=len(self.assets);workflow_steps=['Fetching multi-asset data',f"Processing {num_assets} assets",'Evaluating asset rotation','Making trading decisions','Calculating portfolio metrics']
2050
+ if RICH_AVAILABLE:
2051
+ with Progress(SpinnerColumn(),TextColumn('[progress.description]{task.description}'),BarColumn(),TextColumn('[progress.percentage]{task.percentage:>3.0f}%'),TimeRemainingColumn(),console=Console(),transient=False)as progress:
2052
+ task=progress.add_task('[cyan]Multi-Asset Trading Workflow',total=len(workflow_steps));progress.update(task,description=f"[yellow]{workflow_steps[0]}...");self.fetch_data()
2053
+ if not self.price_data_dict:return{'error':'Failed to fetch multi-asset data'}
2054
+ progress.advance(task)
2055
+ all_signal_scores,all_predictions,all_metadata,all_intervals,all_current_prices={},{},{},{},{};asset_symbols=list(self.price_data_dict.keys());congestion_metric=.0
2056
+ for(idx,symbol)in enumerate(asset_symbols):
2057
+ progress.update(task,description=f"[yellow]Processing asset {idx+1}/{len(asset_symbols)}: {symbol}...");self.price_data=self.price_data_dict[symbol];self.current_price=self.price_data['price'].iloc[-1]if not self.price_data.empty else .0;all_current_prices[symbol]=self.current_price;signals_df=self.compute_signals()
2058
+ if signals_df.empty:continue
2059
+ signal_scores=self.validate_signals();all_signal_scores[symbol]=signal_scores
2060
+ if all_signal_scores:
2061
+ multi_predictions=self.generate_multi_asset_predictions(all_signal_scores)
2062
+ for symbol in asset_symbols:
2063
+ if symbol in multi_predictions:predictions,intervals,metadata=multi_predictions[symbol];all_predictions[symbol]=predictions;all_metadata[symbol]=metadata;all_intervals[symbol]=intervals
2064
+ else:all_predictions[symbol]=np.array([.0]);all_metadata[symbol]={};all_intervals[symbol]={}
2065
+ progress.advance(task);progress.update(task,description=f"[yellow]{workflow_steps[2]}...");rotation_trades={'exits':[],'entries':[]}
2066
+ total_trades=0
2067
+ if ROTATION_ENABLED and self.rotation_manager:
2068
+ predictions_dict={symbol:float(all_predictions[symbol][-1])if symbol in all_predictions and len(all_predictions[symbol])>0 else .0 for symbol in all_current_prices.keys()};confidences_dict={symbol:all_metadata.get(symbol,{}).get('prediction_confidence',.5)for symbol in all_current_prices.keys()};uncertainties_dict={symbol:all_metadata.get(symbol,{}).get('prediction_uncertainty',.5)for symbol in all_current_prices.keys()};portfolio_metrics=self._calculate_portfolio_metrics();rotation_trades=self.rotation_manager.evaluate_asset_rotation(current_positions=self.positions,all_assets=self.assets,predictions=predictions_dict,signal_scores_dict=all_signal_scores,current_prices=all_current_prices,prediction_confidences=confidences_dict,prediction_uncertainties=uncertainties_dict,portfolio_value=portfolio_metrics.get('current_portfolio_value',self.initial_portfolio_value))
2069
+ for exit_info in rotation_trades.get('exits',[]):
2070
+ symbol=exit_info['symbol']
2071
+ if symbol in self.positions and self.live_trading_mode:
2072
+ exit_size=self.positions[symbol].get('size',.0)/portfolio_metrics.get('current_portfolio_value',self.initial_portfolio_value)
2073
+ if exit_size>0:
2074
+ trade_result=self.execution_engine.execute_trade(signal='SELL',size_fraction=min(exit_size,self.risk_monitor.max_position_size),base_symbol=symbol)
2075
+ if trade_result:
2076
+ total_trades+=1
2077
+ if trade_result.get('status')=='success':self._record_live_fill(symbol=symbol,side='SELL',amount=trade_result.get('amount',.0),price=trade_result.get('price',all_current_prices.get(symbol,.0)))
2078
+ logger.debug(f"!!! Rotation: Exited {symbol} (score: {exit_info["score"]:.3f})")if VERBOSE_LOGGING else None
2079
+ for entry_info in rotation_trades.get('entries',[]):
2080
+ symbol=entry_info['symbol']
2081
+ if not any(a.get('symbol','').upper()==symbol for a in self.assets):self.assets.append({'symbol':symbol,'type':'crypto'})
2082
+ progress.advance(task);progress.update(task,description=f"[yellow]{workflow_steps[3]}...")
2083
+ if hasattr(self,'asset_discovery'):
2084
+ asset_scores_list=[]
2085
+ for asset in self.assets:
2086
+ symbol=asset.get('symbol','').upper()
2087
+ if symbol not in self.price_data_dict:continue
2088
+ pred_array=all_predictions.get(symbol,np.array([]));predicted_return=float(pred_array[-1])if len(pred_array)>0 else None;meta=all_metadata.get(symbol,{});priority_score=self.asset_discovery.get_asset_priority_score(symbol,self.price_data_dict[symbol],all_signal_scores.get(symbol,{}),all_current_prices.get(symbol,.0),predicted_return=predicted_return,prediction_confidence=meta.get('prediction_confidence',None),prediction_uncertainty=meta.get('prediction_uncertainty',None));asset_scores_list.append((priority_score,asset))
2089
+ sorted_assets=[asset for(_,asset)in sorted(asset_scores_list,key=lambda x:x[0],reverse=True)]
2090
+ else:sorted_assets=self.assets
2091
+ batch_results,total_trades=[],0
2092
+ for batch_idx in range(0,len(sorted_assets),BATCH_SIZE):
2093
+ batch=sorted_assets[batch_idx:batch_idx+BATCH_SIZE];batch_symbols=[a.get('symbol','').upper()for a in batch]
2094
+ if VERBOSE_LOGGING:logger.debug(f"! Processing batch {batch_idx//BATCH_SIZE+1}: {", ".join(batch_symbols)}")
2095
+ batch_predictions={s:all_predictions.get(s)for s in batch_symbols if s in all_predictions};batch_metadata={s:all_metadata.get(s)for s in batch_symbols if s in all_metadata}
2096
+ if not batch_predictions:continue
2097
+ expected_returns_dict,batch_predictions_arrays={},{}
2098
+ for symbol in batch_symbols:
2099
+ if symbol in batch_predictions:
2100
+ pred_array=batch_predictions[symbol][0]if isinstance(batch_predictions[symbol],tuple)and len(batch_predictions[symbol])>0 else batch_predictions[symbol]
2101
+ if isinstance(pred_array,np.ndarray)and len(pred_array)>0:expected_returns_dict[symbol]=float(pred_array[-1]);batch_predictions_arrays[symbol]=pred_array
2102
+ if not expected_returns_dict:continue
2103
+ batch_returns_dict={symbol:self.price_data_dict[symbol]['price'].pct_change().dropna()for symbol in batch_symbols if symbol in self.price_data_dict and'price'in self.price_data_dict[symbol].columns and len(self.price_data_dict[symbol])>1 and len(self.price_data_dict[symbol]['price'].pct_change().dropna())>0};covariance_df=self.market_data_client.compute_multi_asset_covariance(batch_returns_dict,window=20)if len(batch_returns_dict)>1 else pd.DataFrame(np.eye(len(expected_returns_dict))*.01,index=list(expected_returns_dict.keys()),columns=list(expected_returns_dict.keys()));portfolio_weights=self.optimize_portfolio(expected_returns_dict,covariance_df,all_signal_scores);batch_volatility={}
2104
+ for symbol in batch_symbols:
2105
+ try:
2106
+ if symbol in self.price_data_dict and'price'in self.price_data_dict[symbol].columns and len(self.price_data_dict[symbol])>1:
2107
+ pct_change=self.price_data_dict[symbol]['price'].pct_change().dropna()
2108
+ if len(pct_change)>0:vol=pct_change.std()*np.sqrt(252);batch_volatility[symbol]=float(vol)if not np.isnan(vol)else .02
2109
+ else:batch_volatility[symbol]=.02
2110
+ else:batch_volatility[symbol]=.02
2111
+ except Exception:batch_volatility[symbol]=.02
2112
+ if self.system_state!='COOLDOWN'and self.cooldown_enabled:
2113
+ trigger,reason=self._should_enter_cooldown(api_budget_remaining,congestion_metric)
2114
+ if trigger:self._enter_cooldown(reason)
2115
+ try:batch_decisions=self._make_multi_asset_decisions(portfolio_weights,batch_predictions_arrays,{s:all_signal_scores.get(s,{})for s in batch_symbols},batch_metadata,{s:all_intervals.get(s,{})for s in batch_symbols},batch_volatility,{s:all_current_prices.get(s,.0)for s in batch_symbols},self.risk_monitor.max_position_size)
2116
+ except Exception as e:
2117
+ logger.error(f"Batch decision making failed: {e}");batch_decisions={}
2118
+ for symbol in batch_symbols:
2119
+ confidence,confidence_explanations=self._compute_confidence_only(batch_predictions_arrays.get(symbol,np.array([.0])),all_signal_scores.get(symbol,{}),batch_metadata.get(symbol,{}),all_intervals.get(symbol,{}));prediction=batch_predictions_arrays.get(symbol,np.array([.0]));weight=float(prediction[0])if len(prediction)>0 else .0;signal='HOLD';aggressive_base_threshold=.45;aggressive_weight_threshold=.04
2120
+ if weight>aggressive_weight_threshold and confidence>aggressive_base_threshold:signal='BUY'
2121
+ elif weight<-aggressive_weight_threshold and confidence>aggressive_base_threshold:signal='SELL'
2122
+ batch_decisions[symbol]={'signal':signal,'confidence':confidence,'recommended_position_size':.0,'expected_return':weight*.1,'explanations':confidence_explanations+[f"Position sizing failed: {str(e)}"],'current_price':all_current_prices.get(symbol,.0)}
2123
+ progress.advance(task);progress.update(task,description=f"[yellow]{workflow_steps[3]}...");batch_trades=[]
2124
+ for(symbol,decision)in batch_decisions.items():
2125
+ signal=decision.get('signal','HOLD')
2126
+ if self.system_state in('COOLDOWN','EXIT'):decision=decision.copy()if isinstance(decision,dict)else{};decision['signal']='EXIT'if self.system_state=='EXIT'else'COOLDOWN';decision['recommended_position_size']=.0;explanations=decision.get('explanations',[]);explanations.append(f"[{decision["signal"]}] Observing only; no trades placed");decision['explanations']=explanations;signal=decision['signal']
2127
+ current_price=all_current_prices.get(symbol,decision.get('current_price',.0))
2128
+ if signal not in['HOLD','COOLDOWN','EXIT']and self.live_trading_mode:
2129
+ expected_return_pct=decision.get('expected_return',None);rec_size=decision.get('recommended_position_size',.0)or .0;max_sz=self.risk_monitor.max_position_size if self.risk_monitor and self.risk_monitor.max_position_size is not None else .1;trade_result=self.execution_engine.execute_trade(signal=signal,size_fraction=min(abs(rec_size),max_sz),base_symbol=symbol,aggressive_mode=self.aggressive_mode,expected_return_pct=expected_return_pct)
2130
+ if trade_result:
2131
+ batch_trades.append(trade_result);total_trades+=1
2132
+ if trade_result.get('status')=='success':filled_amt=trade_result.get('amount',.0);filled_price=trade_result.get('price',current_price);self._record_live_fill(symbol=symbol,side=signal,amount=filled_amt,price=filled_price);filled_val=filled_amt*filled_price;portfolio_value=portfolio_metrics.get('current_portfolio_value',self.account_size if self.account_size>0 else 1.);frac=filled_val/portfolio_value if portfolio_value>0 else .0;decision['recommended_position_size']=frac
2133
+ decision_with_symbol=decision.copy()if isinstance(decision,dict)else{};decision_with_symbol['symbol']=symbol
2134
+ if signal not in['HOLD','COOLDOWN']and not self.live_trading_mode:
2135
+ rec_size=decision_with_symbol.get('recommended_position_size',.0)or .0;account_size=getattr(self,'account_size',TRADING_CONFIG.get('account_size',1e3));min_trade_value=getattr(self,'min_trade_value',TRADING_CONFIG.get('min_trade_value',1.));min_fraction=min_trade_value/account_size if account_size>0 else .0;max_fraction=getattr(self.risk_monitor,'max_position_size',TRADING_CONFIG.get('max_position_size',.1))
2136
+ if max_fraction is None or not isinstance(max_fraction,(int,float)):max_fraction=.1
2137
+ portfolio_metrics=self._calculate_portfolio_metrics();current_portfolio_value=portfolio_metrics.get('current_portfolio_value',account_size if account_size>0 else 1.);total_invested=portfolio_metrics.get('total_invested',.0);max_total_exposure=getattr(self,'max_total_exposure',1.);allowed_fraction=max(.0,max_total_exposure-(total_invested/current_portfolio_value if current_portfolio_value>0 else .0));rec_size=max(min_fraction,rec_size);rec_size=min(rec_size,max_fraction,allowed_fraction);decision_with_symbol['recommended_position_size']=rec_size
2138
+ if not self.live_trading_mode:
2139
+ if signal=='COOLDOWN':self._update_positions('HOLD',decision_with_symbol,current_price)
2140
+ else:self._update_positions(signal,decision_with_symbol,current_price)
2141
+ batch_decisions[symbol]=decision_with_symbol
2142
+ batch_results.append({'batch':batch_idx//BATCH_SIZE+1,'assets':batch_symbols,'decisions':batch_decisions,'trades':batch_trades})
2143
+ if batch_idx+BATCH_SIZE<len(sorted_assets):await asyncio.sleep(BATCH_DELAY)
2144
+ progress.update(task,description=f"[yellow]{workflow_steps[4]}...");progress.advance(task);progress.update(task,completed=len(workflow_steps));primary_symbol=list(self.price_data_dict.keys())[0]if self.price_data_dict else None;primary_decision=batch_results[0]['decisions'].get(primary_symbol,{})if batch_results else{};primary_price=all_current_prices.get(primary_symbol,.0)if primary_symbol else .0;primary_metadata=all_metadata.get(primary_symbol,{})if primary_symbol else{};primary_intervals=all_intervals.get(primary_symbol,{})if primary_symbol else{};price_targets={horizon:primary_intervals[horizon].get('mean',primary_price)for horizon in['1d','3d','7d']if horizon in primary_intervals};primary_volatility=.02
2145
+ if primary_symbol and primary_symbol in self.price_data_dict:
2146
+ returns=self.price_data_dict[primary_symbol]['price'].pct_change().dropna()
2147
+ if len(returns)>0:primary_volatility=float(returns.std()*np.sqrt(252))
2148
+ risk_metrics={'volatility':primary_volatility,'sharpe_ratio':primary_decision.get('sharpe_ratio',.0),'cvar_95':primary_decision.get('cvar_95',.0),'prediction_uncertainty':primary_metadata.get('prediction_uncertainty',.0)};regime='unknown'
2149
+ if primary_symbol and primary_symbol in self.price_data_dict:
2150
+ try:
2151
+ price_data=self.price_data_dict[primary_symbol]
2152
+ if not price_data.empty and'price'in price_data.columns:
2153
+ recent_returns=price_data['price'].pct_change().dropna().tail(20)
2154
+ if len(recent_returns)>0:vol,mean_ret=recent_returns.std(),recent_returns.mean();regime='high_volatility'if vol>.05 else'bullish'if mean_ret>.01 else'bearish'if mean_ret<-.01 else'neutral'
2155
+ except Exception:pass
2156
+ if RICH_AVAILABLE:progress.update(task,description=f"[yellow]{workflow_steps[4]}...");progress.advance(task);progress.update(task,completed=len(workflow_steps))
2157
+ wallet_snapshot=self._get_live_wallet_snapshot();return{'signal':primary_decision.get('signal','HOLD'),'original_signal':primary_decision.get('original_signal'),'confidence':primary_decision.get('confidence',.0),'current_price':primary_price,'expected_return':primary_decision.get('expected_return',.0),'recommended_position_size':primary_decision.get('recommended_position_size',.0),'original_position_size':primary_decision.get('original_position_size'),'price_targets':price_targets,'confidence_intervals':primary_intervals,'top_signals':primary_decision.get('top_signals',[]),'signal_explanations':primary_decision.get('explanations',[]),'risk_metrics':risk_metrics,'regime':regime,'entry_level':primary_decision.get('entry_level'),'stop_loss':primary_decision.get('stop_loss'),'take_profit':primary_decision.get('take_profit'),'batched_results':batch_results,'total_assets':len(sorted_assets),'total_trades':total_trades,'portfolio_weights':portfolio_weights if'portfolio_weights'in locals()else{},'all_current_prices':all_current_prices,'all_metadata':all_metadata,'all_intervals':all_intervals,'price_data_dict':{k:v.copy()if isinstance(v,pd.DataFrame)else v for(k,v)in self.price_data_dict.items()},'portfolio_metrics':self._calculate_portfolio_metrics(),'system_state':self.system_state,'wallet':wallet_snapshot};wallet_snapshot=self._get_live_wallet_snapshot();return{'signal':primary_decision.get('signal','HOLD'),'original_signal':primary_decision.get('original_signal'),'confidence':primary_decision.get('confidence',.0),'current_price':primary_price,'expected_return':primary_decision.get('expected_return',.0),'recommended_position_size':primary_decision.get('recommended_position_size',.0),'original_position_size':primary_decision.get('original_position_size'),'price_targets':price_targets,'confidence_intervals':primary_intervals,'top_signals':primary_decision.get('top_signals',[]),'signal_explanations':primary_decision.get('explanations',[]),'risk_metrics':risk_metrics,'regime':regime,'entry_level':primary_decision.get('entry_level'),'stop_loss':primary_decision.get('stop_loss'),'take_profit':primary_decision.get('take_profit'),'batched_results':batch_results,'total_assets':len(sorted_assets),'total_trades':total_trades,'portfolio_weights':portfolio_weights if'portfolio_weights'in locals()else{},'all_current_prices':all_current_prices,'all_metadata':all_metadata,'all_intervals':all_intervals,'price_data_dict':{k:v.copy()if isinstance(v,pd.DataFrame)else v for(k,v)in self.price_data_dict.items()},'portfolio_metrics':portfolio_metrics,'system_state':self.system_state,'wallet':wallet_snapshot}
2158
+ def _update_positions(self,signal:str,decision:Dict[str,Any],current_price:float)->None:
2159
+ import time
2160
+ if not hasattr(self,'positions'):self.positions={}
2161
+ symbol=decision.get('symbol','MAIN')if isinstance(decision,dict)and'symbol'in decision else list(self.price_data_dict.keys())[0]if self.multi_asset_mode and hasattr(self,'price_data_dict')and self.price_data_dict else'MAIN';position_size_pct=abs(decision.get('recommended_position_size',.0))if isinstance(decision,dict)else .0;entry_price=decision.get('entry_level',current_price)if isinstance(decision,dict)else current_price;portfolio_metrics=self._calculate_portfolio_metrics();current_portfolio_value=portfolio_metrics.get('current_portfolio_value',self.initial_portfolio_value);position_value=current_portfolio_value*position_size_pct
2162
+ if signal=='BUY'and position_size_pct>0:
2163
+ if symbol in self.positions:old_size,old_entry=self.positions[symbol]['size'],self.positions[symbol]['entry_price'];new_size=old_size+position_value;new_entry=(old_entry*old_size+entry_price*position_value)/new_size if new_size>0 else entry_price;self.positions[symbol]={'size':new_size,'entry_price':new_entry,'entry_time':self.positions[symbol].get('entry_time',time.time()),'value':new_size,'position_size_pct':new_size/current_portfolio_value if current_portfolio_value>0 else .0}
2164
+ else:self.positions[symbol]={'size':position_value,'entry_price':entry_price,'entry_time':time.time(),'value':position_value,'position_size_pct':position_value/current_portfolio_value if current_portfolio_value>0 else position_size_pct}
2165
+ elif signal=='SELL'and position_size_pct>0:
2166
+ if symbol in self.positions:
2167
+ old_value=self.positions[symbol]['size']
2168
+ if position_value>=old_value:del self.positions[symbol]
2169
+ else:new_value=old_value-position_value;self.positions[symbol]['size']=new_value;self.positions[symbol]['position_size_pct']=new_value/current_portfolio_value if current_portfolio_value>0 else .0
2170
+ self._update_position_values()
2171
+ def _record_live_fill(self,symbol:str,side:str,amount:float,price:float)->None:
2172
+ import time
2173
+ if not hasattr(self,'positions'):self.positions={}
2174
+ if amount<=0 or price<=0:return
2175
+ invested=amount*price
2176
+ if side.upper()=='BUY':
2177
+ if symbol in self.positions:old_inv=self.positions[symbol].get('size',.0);old_entry=self.positions[symbol].get('entry_price',price);new_inv=old_inv+invested;new_entry=(old_entry*old_inv+price*invested)/new_inv if new_inv>0 else price;self.positions[symbol]={'size':new_inv,'entry_price':new_entry,'entry_time':self.positions[symbol].get('entry_time',time.time()),'value':new_inv,'position_size_pct':new_inv/max(self.account_size,1.)}
2178
+ else:self.positions[symbol]={'size':invested,'entry_price':price,'entry_time':time.time(),'value':invested,'position_size_pct':invested/max(self.account_size,1.)}
2179
+ elif symbol in self.positions:
2180
+ old_inv=self.positions[symbol].get('size',.0)
2181
+ if invested>=old_inv:del self.positions[symbol]
2182
+ else:remaining=old_inv-invested;self.positions[symbol]['size']=remaining;self.positions[symbol]['position_size_pct']=remaining/max(self.account_size,1.)
2183
+ self._update_position_values()
2184
+ def _update_position_values(self)->None:
2185
+ if not hasattr(self,'positions'):return
2186
+ current_prices={}
2187
+ if getattr(self,'multi_asset_mode',False)and hasattr(self,'price_data_dict')and self.price_data_dict:current_prices={sym:df['price'].iloc[-1]for(sym,df)in self.price_data_dict.items()if not df.empty and'price'in df.columns}
2188
+ elif hasattr(self,'current_price')and self.current_price>0:current_prices['MAIN']=self.current_price
2189
+ if hasattr(self,'last_full_run')and self.last_full_run is not None:current_prices.update(self.last_full_run.get('all_current_prices',{}))
2190
+ for(sym,position)in list(self.positions.items()):
2191
+ current_price=current_prices.get(sym)or position.get('current_price')
2192
+ if current_price:
2193
+ position['current_price']=current_price;invested_amount,entry_price=position.get('size',.0),position.get('entry_price',current_price)
2194
+ if entry_price>0 and invested_amount>0:position['value']=invested_amount*(current_price/entry_price);position['unrealized_pnl']=position['value']-invested_amount;position['unrealized_pnl_pct']=(current_price-entry_price)/entry_price*100
2195
+ else:position['value'],position['unrealized_pnl'],position['unrealized_pnl_pct']=invested_amount,.0,.0
2196
+ def _promote_baseline(self,current_val:float)->None:
2197
+ new_baseline=max(self.baseline_value,current_val)
2198
+ if new_baseline<=self.baseline_value:return
2199
+ if self.live_trading_mode and self.promotion_debounce_secs>0:
2200
+ now_ts=time.time()
2201
+ if self.promotion_candidate_ts is None or new_baseline>(self.promotion_candidate_value or .0):self.promotion_candidate_value=new_baseline;self.promotion_candidate_ts=now_ts;logger.info(f"[RATCHET] Live debounce started — candidate baseline {self.promotion_candidate_value:,.2f}");return
2202
+ if now_ts-self.promotion_candidate_ts<self.promotion_debounce_secs:logger.info(f"[RATCHET] Live debounce pending ({now_ts-self.promotion_candidate_ts:.2f}s/{self.promotion_debounce_secs:.2f}s) — candidate {self.promotion_candidate_value:,.2f}");return
2203
+ new_baseline=max(new_baseline,self.promotion_candidate_value or new_baseline)
2204
+ self.baseline_value=new_baseline;self.promotion_event=True;self.promotion_candidate_value=None;self.promotion_candidate_ts=None;logger.info(f"[RATCHET] Baseline promoted: {self.baseline_value:,.2f}");self._on_promotion_event()
2205
+ def _on_promotion_event(self)->None:
2206
+ if self.promotion_liquidate_enabled:
2207
+ logger.info('[RATCHET] Promotion event — pausing trading and liquidating positions');self.system_state='COOLDOWN'
2208
+ if self.live_trading_mode and hasattr(self,'positions')and self.positions:
2209
+ for(sym,pos)in list(self.positions.items()):
2210
+ try:self.execution_engine.execute_trade(signal='SELL',size_fraction=1.,base_symbol=sym,aggressive_mode=self.aggressive_mode)
2211
+ except Exception as e:logger.warning(f"[RATCHET] Failed to liquidate {sym}: {e}")
2212
+ elif hasattr(self,'positions'):self.positions={}
2213
+ self.force_halt_after_promotion=True
2214
+ else:self.force_halt_after_promotion=False
2215
+ def _calculate_portfolio_metrics(self)->Dict[str,Any]:
2216
+ if not hasattr(self,'positions'):self.positions={}
2217
+ self._update_position_values();total_invested=sum(pos.get('size',pos.get('value',.0))for pos in self.positions.values());total_unrealized_pnl=sum(pos.get('unrealized_pnl',.0)for pos in self.positions.values());current_portfolio_value=self.initial_portfolio_value+total_unrealized_pnl;base_val=self.initial_portfolio_value if self.initial_portfolio_value>0 else 1.;baseline_val=self.baseline_value if self.baseline_value>0 else base_val;last_val=getattr(self,'last_portfolio_value',base_val);total_return_pct=(current_portfolio_value-base_val)/base_val*100;baseline_return_pct=(current_portfolio_value-baseline_val)/baseline_val*100;return_since_last=(current_portfolio_value-last_val)/last_val*100 if last_val>0 else .0;self.last_portfolio_value,self.total_pnl,self.total_return_pct=current_portfolio_value,total_unrealized_pnl,total_return_pct;self._update_dynamic_position_cap(current_portfolio_value=current_portfolio_value,baseline_value=baseline_val);return{'total_invested':total_invested,'total_unrealized_pnl':total_unrealized_pnl,'current_portfolio_value':current_portfolio_value,'initial_portfolio_value':self.initial_portfolio_value,'baseline_value':self.baseline_value,'total_return_pct':total_return_pct,'baseline_return_pct':baseline_return_pct,'return_since_last':return_since_last,'positions':self.positions.copy()}
2218
+ def _get_live_wallet_snapshot(self)->Dict[str,Any]:
2219
+ if not self.live_trading_mode or not hasattr(self,'execution_engine')or not self.execution_engine:return{}
2220
+ snapshot:Dict[str,Any]={}
2221
+ try:
2222
+ self.execution_engine._update_balance();snapshot['available_balance']=float(getattr(self.execution_engine,'available_balance',.0)or .0)
2223
+ if hasattr(self.execution_engine,'available_balances_by_quote'):by_quote=getattr(self.execution_engine,'available_balances_by_quote',{})or{};snapshot['by_quote']={k:float(v)for(k,v)in by_quote.items()if isinstance(v,(int,float))and abs(v)>0}
2224
+ if hasattr(self.execution_engine,'latest_balances_norm'):bal=getattr(self.execution_engine,'latest_balances_norm',{})or{};snapshot['balances']={k:float(v)for(k,v)in bal.items()if isinstance(v,(int,float))and abs(v)>0}
2225
+ if hasattr(self.execution_engine,'get_live_holdings'):
2226
+ holdings=self.execution_engine.get_live_holdings()
2227
+ if holdings:snapshot['holdings']=holdings
2228
+ holdings=snapshot.get('holdings',{})or snapshot.get('balances',{})or{};totals={'eur':.0,'usd':.0,'details':[]}
2229
+ if holdings and hasattr(self.execution_engine,'exchange')and self.execution_engine.exchange:
2230
+ markets=getattr(self.execution_engine,'markets',{})or{};preferred_quotes=['EUR','USD','USDT','USDC']
2231
+ for(asset,amt)in holdings.items():
2232
+ try:
2233
+ if amt is None or abs(float(amt))==0:continue
2234
+ amt=float(amt);upper_asset=asset.upper()
2235
+ if upper_asset in('EUR',):totals['eur']+=amt;totals['details'].append({'asset':upper_asset,'amount':amt,'quote':'EUR','value':amt});continue
2236
+ if upper_asset in('USD','USDT','USDC'):totals['usd']+=amt;totals['details'].append({'asset':upper_asset,'amount':amt,'quote':'USD','value':amt});continue
2237
+ quote_used=None;last_price=None
2238
+ for q in preferred_quotes:
2239
+ pair=f"{upper_asset}/{q}"
2240
+ if pair in markets:quote_used=q;ticker=self.execution_engine._with_retries(self.execution_engine.exchange.fetch_ticker,pair);last_price=float(ticker.get('last')or .0);break
2241
+ if quote_used and last_price and last_price>0:
2242
+ val=amt*last_price
2243
+ if quote_used=='EUR':totals['eur']+=val
2244
+ else:totals['usd']+=val
2245
+ totals['details'].append({'asset':upper_asset,'amount':amt,'quote':quote_used,'value':val})
2246
+ except Exception:continue
2247
+ snapshot['totals']=totals;snapshot['exchange']=getattr(self.execution_engine,'exchange_name','')
2248
+ except Exception as e:logger.warning(f"[WALLET] Failed to snapshot live balances: {e}")
2249
+ return snapshot
2250
+ def _update_session_pnl(self,portfolio_metrics:Optional[Dict[str,Any]]=None)->Dict[str,Any]:
2251
+ metrics=portfolio_metrics or self._calculate_portfolio_metrics();current_value=metrics.get('current_portfolio_value',self.initial_portfolio_value)
2252
+ if self.initial_portfolio_value<=0:self.initial_portfolio_value=current_value if current_value>0 else 1.
2253
+ self.session_pnl_pct=(current_value-self.initial_portfolio_value)/self.initial_portfolio_value*100 if self.initial_portfolio_value>0 else .0;baseline_pnl_pct=(current_value-self.baseline_value)/self.baseline_value*100 if self.baseline_value>0 else .0
2254
+ if current_value>self.baseline_value:self._promote_baseline(current_value)
2255
+ elif self.live_trading_mode and self.promotion_debounce_secs>0:self.promotion_candidate_value=None;self.promotion_candidate_ts=None
2256
+ return metrics
2257
+ def _compute_dynamic_position_cap(self,baseline_value:float)->float:
2258
+ hard_cap=TRADING_CONFIG.get('max_position_hard_cap',.3);tiers=[(100,.01),(500,.015),(2000,.02),(10000,.025),(float('inf'),.03)];cap=TRADING_CONFIG.get('max_position_size',.2)
2259
+ for(threshold,tier_cap)in tiers:
2260
+ if baseline_value<threshold:cap=tier_cap;break
2261
+ cap=min(cap,.03*baseline_value**.5/100**.5)if baseline_value>0 else cap;cap=min(cap,hard_cap);cap=max(self.dynamic_max_position_size,cap);return cap
2262
+ def _update_dynamic_position_cap(self,current_portfolio_value:float,baseline_value:float)->None:
2263
+ if getattr(self,'system_state','ACTIVE')!='ACTIVE':return
2264
+ new_cap=self._compute_dynamic_position_cap(baseline_value)
2265
+ if new_cap>self.dynamic_max_position_size:
2266
+ self.dynamic_max_position_size=new_cap
2267
+ if hasattr(self,'risk_monitor')and self.risk_monitor:self.risk_monitor.max_position_size=new_cap
2268
+ if hasattr(self,'execution_engine')and self.execution_engine:self.execution_engine.max_position_size=new_cap
2269
+ logger.info(f"[RISK] Max position cap increased to {new_cap:.2%} based on baseline {baseline_value:,.2f}")
2270
+ def _maybe_trigger_session_circuit_breaker(self,portfolio_metrics:Dict[str,Any])->None:
2271
+ if self.circuit_breaker_triggered or not self.cooldown_enabled:return
2272
+ pnl=self.session_pnl_pct
2273
+ if pnl<=self.stop_loss_pct or pnl>=self.stop_gain_pct:self.circuit_breaker_triggered=True;reason=f"[CIRCUIT] Triggered: session_pnl={pnl:+.2f}%";logger.warning(reason);self._enter_cooldown(reason)
2274
+ def _deterministic_cooldown_loops(self)->int:span=max(1,self.cooldown_max_loops-self.cooldown_min_loops+1);return self.cooldown_min_loops+self.global_loop_counter%span
2275
+ def _should_enter_cooldown(self,api_budget_remaining:float,congestion_metric:float)->Tuple[bool,str]:
2276
+ if not self.cooldown_enabled or self.system_state=='COOLDOWN'or self.cooldown_exit_guard:return False,''
2277
+ reasons=[]
2278
+ if self.loops_since_cooldown>=self.cooldown_loop_trigger:reasons.append(f"loops_since_cooldown={self.loops_since_cooldown} >= {self.cooldown_loop_trigger}")
2279
+ if api_budget_remaining<self.cooldown_api_budget_threshold:reasons.append(f"api_budget={api_budget_remaining:.0%} < {self.cooldown_api_budget_threshold:.0%}")
2280
+ if congestion_metric>self.cooldown_congestion_threshold:reasons.append(f"signal_congestion={congestion_metric:.2f} > {self.cooldown_congestion_threshold:.2f}")
2281
+ if reasons:return True,'; '.join(reasons)
2282
+ return False,''
2283
+ def _enter_cooldown(self,reason:str)->None:
2284
+ self.system_state='COOLDOWN';self.cooldown_trigger_reason=reason;self.cooldown_total_loops=self._deterministic_cooldown_loops();self.cooldown_loops_remaining=self.cooldown_total_loops;self.loops_since_cooldown=0;metrics=self._calculate_portfolio_metrics();current_val=metrics.get('current_portfolio_value',self.baseline_value);self.cooldown_peak_value=current_val;baseline_pnl_pct=(current_val-self.baseline_value)/self.baseline_value*100 if self.baseline_value>0 else .0;logger.info(f"[RATCHET] Entry check — baseline={self.baseline_value:,.2f}, current={current_val:,.2f}, pnl={baseline_pnl_pct:+.2f}%")
2285
+ if current_val>self.baseline_value:self._promote_baseline(current_val)
2286
+ logger.info(f"[COOLDOWN] Triggered: {reason} — duration {self.cooldown_total_loops} loops")
2287
+ def _cooldown_tick(self,api_budget_remaining:float)->None:
2288
+ if self.system_state!='COOLDOWN':return
2289
+ completed=self.cooldown_total_loops-self.cooldown_loops_remaining+1;logger.info(f"[COOLDOWN] Loop {completed}/{self.cooldown_total_loops} — observing only (reason: {self.cooldown_trigger_reason})");metrics=self._calculate_portfolio_metrics();current_val=metrics.get('current_portfolio_value',self.baseline_value)
2290
+ if self.cooldown_peak_value is None:self.cooldown_peak_value=current_val
2291
+ else:self.cooldown_peak_value=max(self.cooldown_peak_value,current_val)
2292
+ baseline_pnl_peak=(self.cooldown_peak_value-self.baseline_value)/self.baseline_value*100 if self.baseline_value>0 else .0
2293
+ if self.cooldown_peak_value>self.baseline_value:logger.info(f"[RATCHET] New peak during cooldown: peak pnl {baseline_pnl_peak:+.2f}%")
2294
+ self.cooldown_loops_remaining=max(0,self.cooldown_loops_remaining-1)
2295
+ if self.cooldown_loops_remaining<=0:
2296
+ logger.info('[COOLDOWN] Completed');peak_val=self.cooldown_peak_value if self.cooldown_peak_value is not None else current_val;baseline_pnl_pct=(peak_val-self.baseline_value)/self.baseline_value*100 if self.baseline_value>0 else .0;logger.info(f"[RATCHET] Exit check — baseline={self.baseline_value:,.2f}, peak={peak_val:,.2f}, peak pnl={baseline_pnl_pct:+.2f}%")
2297
+ if peak_val>self.baseline_value:self._promote_baseline(peak_val)
2298
+ self.cooldown_peak_value=None;self.circuit_breaker_triggered=False;self.system_state='ACTIVE';self.cooldown_trigger_reason='';self.cooldown_exit_guard=True;logger.info('[COOLDOWN] Exit — resuming ACTIVE state')
2299
+ if self.force_halt_after_promotion:logger.info('[RATCHET] Promotion halt engaged — further trading paused');raise SystemExit(0)
2300
+ async def _exit_liquidate_all(self,target_quotes:Optional[List[str]]=None)->Dict[str,Any]:
2301
+ self.system_state='EXIT';self.cooldown_trigger_reason='EXIT';target_quotes=target_quotes or['EUR','USD'];summary:Dict[str,Any]={'mode':'EXIT','live':self.live_trading_mode,'actions':[]}
2302
+ try:summary['before']=self._calculate_portfolio_metrics()
2303
+ except Exception:summary['before']={}
2304
+ if not self.live_trading_mode:summary['note']='Demo mode: positions not liquidated';self.exit_summary=summary;return summary
2305
+ if not hasattr(self,'positions')or not self.positions:summary['note']='No positions to liquidate';self.exit_summary=summary;return summary
2306
+ loop=asyncio.get_event_loop();liquidation_tasks=[]
2307
+ for(sym,pos)in list(self.positions.items()):
2308
+ async def sell_symbol(symbol:str)->Dict[str,Any]:
2309
+ def do_sell()->Dict[str,Any]:
2310
+ try:return{'symbol':symbol,'result':self.execution_engine.execute_trade(signal='SELL',size_fraction=1.,base_symbol=symbol,aggressive_mode=self.aggressive_mode)}
2311
+ except Exception as e:return{'symbol':symbol,'error':str(e)}
2312
+ return await loop.run_in_executor(None,do_sell)
2313
+ liquidation_tasks.append(sell_symbol(sym))
2314
+ actions=await asyncio.gather(*liquidation_tasks,return_exceptions=False);summary['actions']=actions
2315
+ try:summary['after']=self._calculate_portfolio_metrics()
2316
+ except Exception:summary['after']={}
2317
+ self.exit_summary=summary;logger.info(f"[EXIT] Liquidation summary: {summary}");return summary
2318
+ def _incremental_update(self)->Dict[str,Any]:
2319
+ if self.last_full_run is None:return{'error':'No cached data available for incremental update'}
2320
+ return self.last_full_run
2321
+ def _init_cache(self)->str:
2322
+ if self.cache_type=='redis'and REDIS_AVAILABLE:
2323
+ try:self._redis_client=redis.Redis(host='localhost',port=6379,db=0,decode_responses=True);self._redis_client.ping();return'redis'
2324
+ except Exception:return'file'
2325
+ return'file'
2326
+ def _get_cache(self,key:str)->Optional[Any]:
2327
+ if self.cache=='redis'and self._redis_client:
2328
+ try:
2329
+ cached=self._redis_client.get(key)
2330
+ if cached:return json.loads(cached)
2331
+ except Exception:pass
2332
+ cache_file=self.cache_dir/f"{key.replace(":","_")}.json"
2333
+ if cache_file.exists():
2334
+ try:
2335
+ with open(cache_file,'r')as f:
2336
+ data=json.load(f)
2337
+ if'timestamp'in data and'ttl'in data:
2338
+ if time.time()-data['timestamp']<data['ttl']:return data['data']
2339
+ else:return data
2340
+ except Exception:pass
2341
+ def _cache_data(self,key:str,data:Any,ttl:int=3600):
2342
+ cache_data={'data':data,'timestamp':time.time(),'ttl':ttl}
2343
+ if self.cache=='redis'and self._redis_client:
2344
+ try:self._redis_client.setex(key,ttl,json.dumps(cache_data));return
2345
+ except Exception:pass
2346
+ cache_file=self.cache_dir/f"{key.replace(":","_")}.json"
2347
+ try:
2348
+ with open(cache_file,'w')as f:json.dump(cache_data,f)
2349
+ except Exception:pass
2350
+ async def run(self,incremental:bool=False)->Dict[str,Any]:
2351
+ import time;self.global_loop_counter+=1
2352
+ if self.system_state!='COOLDOWN':
2353
+ self.loops_since_cooldown+=1
2354
+ if self.cooldown_exit_guard:self.cooldown_exit_guard=False
2355
+ api_budget_remaining=getattr(self,'api_budget_remaining',1.)
2356
+ if self.system_state=='COOLDOWN':self._cooldown_tick(api_budget_remaining)
2357
+ portfolio_metrics=self._update_session_pnl();self._maybe_trigger_session_circuit_breaker(portfolio_metrics);current_time=time.time();time_since_full_run=current_time-getattr(self,'last_full_run_time',.0)if getattr(self,'last_full_run_time',.0)>0 else float('inf')
2358
+ if incremental and self.last_full_run is not None and time_since_full_run<self.full_refresh_interval:
2359
+ if VERBOSE_LOGGING:logger.debug('Performing incremental update (reusing cached data)...')
2360
+ return self._incremental_update()
2361
+ self.last_full_run_time=current_time
2362
+ if self.auto_multi_asset and self.multi_asset_mode:return await self.run_batched_multi_asset()
2363
+ workflow_steps=['Fetching market data','Computing quant signals','Validating signals (CRCA)','Generating predictions','Optimizing portfolio','Calculating price targets','Making trading decisions'];result=None
2364
+ if RICH_AVAILABLE:
2365
+ with Progress(SpinnerColumn(),TextColumn('[progress.description]{task.description}'),BarColumn(),TextColumn('[progress.percentage]{task.percentage:>3.0f}%'),TimeRemainingColumn(),console=Console(),transient=False)as progress:
2366
+ task=progress.add_task('[cyan]Quant Trading Workflow',total=len(workflow_steps));progress.update(task,description=f"[yellow]{workflow_steps[0]}...");self.fetch_data()
2367
+ if self.price_data.empty:return{'error':'No data available'}
2368
+ progress.advance(task);progress.update(task,description=f"[yellow]{workflow_steps[1]}...");signals_df=self.compute_signals()
2369
+ if signals_df.empty:return{'error':'Signal computation failed'}
2370
+ progress.advance(task);progress.update(task,description=f"[yellow]{workflow_steps[2]}...");signal_scores=self.validate_signals();progress.advance(task);progress.update(task,description=f"[yellow]{workflow_steps[3]}...")
2371
+ if self.multi_asset_mode:
2372
+ multi_predictions=self.generate_multi_asset_predictions(signal_scores)
2373
+ if not multi_predictions:return{'error':'Multi-asset prediction generation failed'}
2374
+ primary_symbol=list(self.price_data_dict.keys())[0]
2375
+ if primary_symbol in multi_predictions:pred_array,intervals,metadata=multi_predictions[primary_symbol];predictions,pred_metadata,covariance=pred_array,metadata,np.array([[.01]])
2376
+ else:return{'error':'Primary asset prediction not found'}
2377
+ else:
2378
+ predictions,covariance,pred_metadata=self.generate_predictions(signal_scores)
2379
+ if len(predictions)==0:return{'error':'Prediction generation failed'}
2380
+ progress.advance(task);progress.update(task,description=f"[yellow]{workflow_steps[4]}...");signal_names=list(signal_scores.keys())[:10];recent_perf={name:score.get('score',.0)for(name,score)in signal_scores.items()};regime=self.regime_detector.detect_volatility_regime(self.signals_df);model_names=list(self.ensemble_predictor.models.keys())
2381
+ if signal_names and model_names:weights,best_model=self.meta_learner.optimize(signal_names,recent_perf,regime,model_names);self.ensemble_predictor.update_weights(weights)
2382
+ else:weights,best_model={},model_names[0]if model_names else None
2383
+ progress.advance(task);progress.update(task,description=f"[yellow]{workflow_steps[5]}...");latest_pred=predictions[-1]if len(predictions)>0 else .0;pred_std=pred_metadata.get('prediction_std',[]);latest_std=pred_std[-1]if len(pred_std)>0 else abs(latest_pred)*.1;current_price=self.current_price;price_targets={'1d':current_price*(1+latest_pred),'3d':current_price*(1+latest_pred*3),'7d':current_price*(1+latest_pred*7)};confidence_intervals={'1d':{'lower':current_price*(1+latest_pred-1.96*latest_std),'upper':current_price*(1+latest_pred+1.96*latest_std),'mean':price_targets['1d']},'3d':{'lower':current_price*(1+latest_pred*3-1.96*latest_std*np.sqrt(3)),'upper':current_price*(1+latest_pred*3+1.96*latest_std*np.sqrt(3)),'mean':price_targets['3d']},'7d':{'lower':current_price*(1+latest_pred*7-1.96*latest_std*np.sqrt(7)),'upper':current_price*(1+latest_pred*7+1.96*latest_std*np.sqrt(7)),'mean':price_targets['7d']}};progress.advance(task);progress.update(task,description=f"[yellow]{workflow_steps[6]}...");covariance_df=pd.DataFrame();expected_returns_dict,current_prices_dict={},{};volatility_dict,pred_metadata_dict={},{};confidence_intervals_dict,multi_predictions={},{}
2384
+ if self.multi_asset_mode and'multi_predictions'in locals():expected_returns_dict={symbol:float(pred_array[-1])if len(pred_array)>0 else .0 for(symbol,(pred_array,_,_))in multi_predictions.items()};current_prices_dict={symbol:self.price_data_dict[symbol]['price'].iloc[-1]for symbol in multi_predictions.keys()};volatility_dict={symbol:metadata.get('prediction_uncertainty',.02)for(symbol,(_,_,metadata))in multi_predictions.items()};pred_metadata_dict={symbol:metadata for(symbol,(_,_,metadata))in multi_predictions.items()};confidence_intervals_dict={symbol:intervals for(symbol,(_,intervals,_))in multi_predictions.items()};returns_dict={symbol:df['returns']for(symbol,df)in self.price_data_dict.items()if'returns'in df.columns};covariance_df=self.cov_estimator.compute_cross_asset_covariance(returns_dict)if len(returns_dict)>1 else pd.DataFrame()
2385
+ if not covariance_df.empty:
2386
+ try:
2387
+ corr_matrix=np.corrcoef(covariance_df.values)
2388
+ if corr_matrix.shape[0]>1:upper=corr_matrix[np.triu_indices_from(corr_matrix,k=1)];congestion_metric=float(np.mean(np.abs(upper)))
2389
+ except Exception:congestion_metric=.0;portfolio_weights_dict=self.optimize_portfolio(expected_returns_dict,covariance_df)if not covariance_df.empty else{symbol:.0 for symbol in expected_returns_dict.keys()};decisions_dict=self._make_multi_asset_decisions(portfolio_weights=portfolio_weights_dict,predictions={symbol:pred for(symbol,(pred,_,_))in multi_predictions.items()},signal_scores=signal_scores,pred_metadata_dict=pred_metadata_dict,confidence_intervals_dict=confidence_intervals_dict,volatility_dict=volatility_dict,current_prices=current_prices_dict,max_position_size=self.risk_monitor.max_position_size);primary_symbol=list(self.price_data_dict.keys())[0];primary_decision=decisions_dict.get(primary_symbol,{});progress.update(task,completed=len(workflow_steps));return{'signal':primary_decision.get('signal','HOLD'),'confidence':primary_decision.get('confidence',.0),'current_price':current_prices_dict.get(primary_symbol,self.current_price),'price_targets':{symbol:current_prices_dict[symbol]*(1+expected_returns_dict[symbol])for symbol in current_prices_dict.keys()},'confidence_intervals':confidence_intervals_dict,'risk_metrics':{'volatility':{symbol:vol for(symbol,vol)in volatility_dict.items()},'prediction_uncertainty':{symbol:meta.get('prediction_uncertainty',.0)for(symbol,meta)in pred_metadata_dict.items()}},'signal_explanations':primary_decision.get('explanations',[]),'top_signals':primary_decision.get('top_signals',[]),'recommended_position_size':primary_decision.get('recommended_position_size',.0),'entry_level':primary_decision.get('entry_level'),'stop_loss':primary_decision.get('stop_loss'),'take_profit':primary_decision.get('take_profit'),'portfolio_weights':portfolio_weights_dict,'decisions':decisions_dict,'multi_asset_mode':True,'portfolio_metrics':self._calculate_portfolio_metrics()}
2390
+ else:
2391
+ expected_returns=np.array([latest_pred]);cov_size=CovarianceEstimator.CovSize(covariance);portfolio_weights=self.optimize_portfolio(expected_returns,covariance)if cov_size>0 else np.array([.0])
2392
+ if'returns'in self.signals_df.columns:returns=self.signals_df['returns'].dropna();volatility=returns.std()*np.sqrt(252)if len(returns)>1 else .0;sharpe_ratio=returns.mean()*252/(volatility+1e-06)if len(returns)>1 and volatility>0 else .0;cvar_95=returns[returns<=np.percentile(returns,5)].mean()if len(returns)>10 else returns.mean()-1.65*returns.std()if len(returns)>1 else .0
2393
+ else:volatility,sharpe_ratio,cvar_95=.0,.0,.0
2394
+ decision_result=self._make_trading_decision(portfolio_weights=portfolio_weights,predictions=predictions,signal_scores=signal_scores,pred_metadata=pred_metadata,confidence_intervals=confidence_intervals,volatility=volatility,current_price=current_price,max_position_size=self.risk_monitor.max_position_size);signal,confidence,signal_explanations=decision_result['signal'],decision_result['confidence'],decision_result['explanations'];original_signal,original_position_size=signal,decision_result['recommended_position_size'];circuit_ok,circuit_msg=self.circuit_breaker.check_circuit()
2395
+ if not circuit_ok:logger.warning(f"Circuit breaker tripped: {circuit_msg}");signal_explanations.insert(0,f"!!! Circuit breaker activated: {circuit_msg}");progress.update(task,completed=len(workflow_steps));return{'error':f"Circuit breaker: {circuit_msg}",'signal':'HOLD','original_signal':original_signal,'current_price':current_price,'signal_explanations':signal_explanations,'monitoring_summary':self.monitoring.get_summary()}
2396
+ if signal!='HOLD':
2397
+ position_size=abs(decision_result['recommended_position_size']);portfolio_value=portfolio_metrics.get('current_portfolio_value',getattr(self,'account_size',current_price));current_positions=self.positions if hasattr(self,'positions')else{};risk_ok,risk_msg=self.risk_monitor.pre_trade_check(signal=signal,position_size=position_size,current_positions=current_positions,portfolio_value=portfolio_value)
2398
+ if not risk_ok:
2399
+ max_allowed_size=self.risk_monitor.max_position_size
2400
+ if position_size>max_allowed_size:decision_result['recommended_position_size']=np.sign(decision_result['recommended_position_size'])*max_allowed_size;signal_explanations.append(f"āš ļø Position size adjusted from {position_size:.2%} to {max_allowed_size:.2%} due to risk limits")
2401
+ else:signal='HOLD';signal_explanations.insert(0,f"!!! Trade blocked: {risk_msg}")
2402
+ self.monitoring.monitor_signal_health(signal_name='ensemble',score=confidence,decay=pred_metadata.get('prediction_std',[.0])[-1]if pred_metadata.get('prediction_std')else .0);progress.update(task,completed=len(workflow_steps))
2403
+ return{'signal':signal,'original_signal':original_signal if signal!=original_signal else None,'confidence':confidence,'current_price':current_price,'price_targets':price_targets,'confidence_intervals':confidence_intervals,'signal_explanations':signal_explanations,'top_signals':sorted([(k,v.get('score',.0))for(k,v)in signal_scores.items()],key=lambda x:x[1],reverse=True)[:10],'signal_contributions':pred_metadata.get('signal_contributions',{}),'risk_metrics':{'volatility':volatility,'sharpe_ratio':sharpe_ratio,'cvar_95':cvar_95,'prediction_uncertainty':latest_std},'portfolio_weight':portfolio_weights[0]if len(portfolio_weights)>0 else .0,'recommended_position_size':decision_result['recommended_position_size'],'original_position_size':original_position_size if abs(original_position_size)!=abs(decision_result['recommended_position_size'])else None,'expected_return':latest_pred,'predictions':predictions.tolist()if len(predictions)>0 else[],'regime':self.regime_detector.detect_volatility_regime(self.signals_df),'entry_level':decision_result.get('entry_level'),'stop_loss':decision_result.get('stop_loss'),'take_profit':decision_result.get('take_profit'),'monitoring_summary':self.monitoring.get_summary(),'portfolio_metrics':self._calculate_portfolio_metrics()}
2404
+ else:
2405
+ self.fetch_data()
2406
+ if self.price_data.empty:return{'error':'No data available'}
2407
+ signals_df=self.compute_signals()
2408
+ if signals_df.empty:return{'error':'Signal computation failed'}
2409
+ signal_scores=self.validate_signals()
2410
+ if self.multi_asset_mode:
2411
+ multi_predictions=self.generate_multi_asset_predictions(signal_scores)
2412
+ if not multi_predictions:return{'error':'Multi-asset prediction generation failed'}
2413
+ primary_symbol=list(self.price_data_dict.keys())[0]
2414
+ if primary_symbol in multi_predictions:pred_array,intervals,metadata=multi_predictions[primary_symbol];predictions,pred_metadata,covariance=pred_array,metadata,np.array([[.01]])
2415
+ else:return{'error':'Primary asset prediction not found'}
2416
+ else:
2417
+ predictions,covariance,pred_metadata=self.generate_predictions(signal_scores)
2418
+ if len(predictions)==0:return{'error':'Prediction generation failed'}
2419
+ signal_names=list(signal_scores.keys())[:10];recent_perf={name:score.get('score',.0)for(name,score)in signal_scores.items()};regime=self.regime_detector.detect_volatility_regime(self.signals_df);model_names=list(self.ensemble_predictor.models.keys())
2420
+ if signal_names and model_names:weights,best_model=self.meta_learner.optimize(signal_names,recent_perf,regime,model_names);self.ensemble_predictor.update_weights(weights)
2421
+ else:weights,best_model={},model_names[0]if model_names else None
2422
+ latest_pred=predictions[-1]if len(predictions)>0 else .0;pred_std=pred_metadata.get('prediction_std',[]);latest_std=pred_std[-1]if len(pred_std)>0 else abs(latest_pred)*.1;current_price=self.current_price;pred_return=latest_pred/current_price if abs(latest_pred)>1. and current_price>0 else latest_pred;pred_std_return=latest_std/current_price if abs(latest_pred)>1. and current_price>0 else latest_std;price_targets,confidence_intervals={},{}
2423
+ for horizon in[1,3,7]:simulated_prices=np.array([current_price*np.exp(np.sum(np.random.normal(pred_return,pred_std_return,horizon)))for _ in range(1000)]);price_targets[f"{horizon}d"]=float(np.mean(simulated_prices));confidence_intervals[f"{horizon}d"]={'lower':float(np.percentile(simulated_prices,2.5)),'upper':float(np.percentile(simulated_prices,97.5)),'mean':float(np.mean(simulated_prices)),'median':float(np.median(simulated_prices)),'std':float(np.std(simulated_prices))}
2424
+ if self.multi_asset_mode and'multi_predictions'in locals():
2425
+ expected_returns_dict={symbol:float(pred_array[-1])if len(pred_array)>0 else .0 for(symbol,(pred_array,_,_))in multi_predictions.items()};current_prices_dict={symbol:self.price_data_dict[symbol]['price'].iloc[-1]for symbol in multi_predictions.keys()};volatility_dict={symbol:metadata.get('prediction_uncertainty',.02)for(symbol,(_,_,metadata))in multi_predictions.items()};pred_metadata_dict={symbol:metadata for(symbol,(_,_,metadata))in multi_predictions.items()};confidence_intervals_dict={symbol:intervals for(symbol,(_,intervals,_))in multi_predictions.items()};returns_dict={symbol:df['returns']for(symbol,df)in self.price_data_dict.items()if'returns'in df.columns};covariance_df=self.cov_estimator.compute_cross_asset_covariance(returns_dict)if len(returns_dict)>1 else pd.DataFrame();portfolio_weights_dict=self.optimize_portfolio(expected_returns_dict,covariance_df)if not covariance_df.empty else{symbol:.0 for symbol in expected_returns_dict.keys()};weight_values=list(portfolio_weights_dict.values())
2426
+ if len(weight_values)>1:
2427
+ weight_std,weight_mean_abs=np.std(weight_values),np.mean(np.abs(weight_values))
2428
+ if weight_mean_abs>1e-06 and weight_std/weight_mean_abs<.01:
2429
+ if VERBOSE_LOGGING:logger.warning(f"Portfolio weights too similar (std={weight_std:.6f}, mean={weight_mean_abs:.6f}). Adding differentiation.")
2430
+ for symbol in portfolio_weights_dict:expected_ret=expected_returns_dict.get(symbol,.0);portfolio_weights_dict[symbol]+=expected_ret*.05 if abs(expected_ret)>1e-06 else .0
2431
+ total_abs_weight=sum(abs(w)for w in portfolio_weights_dict.values())
2432
+ if total_abs_weight>1.:portfolio_weights_dict={k:v*(1./total_abs_weight)for(k,v)in portfolio_weights_dict.items()}
2433
+ decisions_dict=self._make_multi_asset_decisions(portfolio_weights=portfolio_weights_dict,predictions={symbol:pred for(symbol,(pred,_,_))in multi_predictions.items()},signal_scores=signal_scores,pred_metadata_dict=pred_metadata_dict,confidence_intervals_dict=confidence_intervals_dict,volatility_dict=volatility_dict,current_prices=current_prices_dict,max_position_size=self.risk_monitor.max_position_size);primary_symbol=list(self.price_data_dict.keys())[0];primary_decision=decisions_dict.get(primary_symbol,{})
2434
+ causal_score,causal_block=self._evaluate_causal_stability();result={'signal':primary_decision.get('signal','HOLD'),'confidence':primary_decision.get('confidence',.0),'current_price':current_prices_dict.get(primary_symbol,self.current_price),'price_targets':{symbol:current_prices_dict[symbol]*(1+expected_returns_dict[symbol])for symbol in current_prices_dict.keys()},'confidence_intervals':confidence_intervals_dict,'risk_metrics':{'volatility':{symbol:vol for(symbol,vol)in volatility_dict.items()},'prediction_uncertainty':{symbol:meta.get('prediction_uncertainty',.0)for(symbol,meta)in pred_metadata_dict.items()}},'signal_explanations':primary_decision.get('explanations',[]),'top_signals':primary_decision.get('top_signals',[]),'recommended_position_size':primary_decision.get('recommended_position_size',.0),'entry_level':primary_decision.get('entry_level'),'stop_loss':primary_decision.get('stop_loss'),'take_profit':primary_decision.get('take_profit'),'portfolio_weights':portfolio_weights_dict,'decisions':decisions_dict,'multi_asset_mode':True,'portfolio_metrics':self._calculate_portfolio_metrics(),'causal_score':causal_score,'causal_block':causal_block}
2435
+ if not isinstance(result,dict):logger.error(f"ERROR: run_batched_multi_asset() about to return {type(result)} instead of dict!");return{'error':f"Invalid return type in run_batched_multi_asset(): {type(result)}"}
2436
+ self.last_full_run=result;return result;expected_returns=np.array([latest_pred]);cov_size=CovarianceEstimator.CovSize(covariance);portfolio_weights=self.optimize_portfolio(expected_returns,covariance)if cov_size>0 else np.array([.0])
2437
+ if'returns'in self.signals_df.columns:returns=self.signals_df['returns'].dropna();volatility=returns.std()*np.sqrt(252)if len(returns)>1 else .0;sharpe_ratio=returns.mean()*252/(volatility+1e-06)if len(returns)>1 and volatility>0 else .0;cvar_95=returns[returns<=np.percentile(returns,5)].mean()if len(returns)>10 else returns.mean()-1.65*returns.std()if len(returns)>1 else .0
2438
+ else:volatility,sharpe_ratio,cvar_95=.0,.0,.0
2439
+ congestion_metric=.0
2440
+ if self.system_state!='COOLDOWN'and self.cooldown_enabled:
2441
+ trigger,reason=self._should_enter_cooldown(api_budget_remaining,congestion_metric)
2442
+ if trigger:self._enter_cooldown(reason)
2443
+ decision_result=self._make_trading_decision(portfolio_weights=portfolio_weights,predictions=predictions,signal_scores=signal_scores,pred_metadata=pred_metadata,confidence_intervals=confidence_intervals,volatility=volatility,current_price=current_price,max_position_size=self.risk_monitor.max_position_size);signal,confidence,signal_explanations=decision_result['signal'],decision_result['confidence'],decision_result['explanations']
2444
+ if self.system_state in('COOLDOWN','EXIT'):signal='EXIT'if self.system_state=='EXIT'else'COOLDOWN';decision_result['recommended_position_size']=.0;decision_result['explanations']=signal_explanations+[f"[{signal}] Observing only; no trades placed"]
2445
+ original_signal,original_position_size=signal,decision_result['recommended_position_size'];circuit_ok,circuit_msg=self.circuit_breaker.check_circuit()
2446
+ if not circuit_ok:logger.warning(f"Circuit breaker tripped: {circuit_msg}");signal_explanations.insert(0,f"!!! Circuit breaker activated: {circuit_msg}");return{'error':f"Circuit breaker: {circuit_msg}",'signal':'HOLD','original_signal':original_signal,'current_price':current_price,'signal_explanations':signal_explanations,'monitoring_summary':self.monitoring.get_summary()}
2447
+ if signal not in['HOLD','COOLDOWN']:
2448
+ position_size=abs(decision_result['recommended_position_size']);portfolio_value=decision_result.get('portfolio_metrics',{}).get('current_portfolio_value',getattr(self,'account_size',current_price));current_positions=self.positions if hasattr(self,'positions')else{};risk_ok,risk_msg=self.risk_monitor.pre_trade_check(signal=signal,position_size=position_size,current_positions=current_positions,portfolio_value=portfolio_value)
2449
+ if not risk_ok:
2450
+ max_allowed_size=self.risk_monitor.max_position_size
2451
+ if position_size>max_allowed_size:decision_result['recommended_position_size']=np.sign(decision_result['recommended_position_size'])*max_allowed_size;signal_explanations.append(f"!!! Position size adjusted from {position_size:.2%} to {max_allowed_size:.2%} due to risk limits");logger.info(f"Position size adjusted from {position_size:.2%} to {max_allowed_size:.2%}")
2452
+ else:signal='HOLD';signal_explanations.insert(0,f"!!! Trade blocked: {risk_msg}");logger.warning(f"Risk check failed: {risk_msg}")
2453
+ if signal!=original_signal and original_signal!='HOLD':
2454
+ if signal_explanations and signal_explanations[0].startswith(original_signal):signal_explanations[0]=f"!!! {signal} signal: Changed from {original_signal} due to risk management ({confidence:.0%} confidence)"
2455
+ trade_result=None
2456
+ if self.live_trading_mode and signal not in['HOLD','COOLDOWN']:
2457
+ logger.warning(f"!!! LIVE TRADING MODE ENABLED - Executing {signal} trade");expected_return_pct=decision_result.get('expected_return',None);rec_sz=decision_result.get('recommended_position_size',.0)or .0;trade_result=self.execution_engine.execute_trade(signal=signal,size_fraction=min(abs(rec_sz),self.execution_engine.max_position_size),base_symbol=primary_symbol,aggressive_mode=self.aggressive_mode,expected_return_pct=expected_return_pct)
2458
+ if trade_result and trade_result.get('status')=='success':filled_amt=trade_result.get('amount',.0);filled_price=trade_result.get('price',current_price);self._record_live_fill(symbol=primary_symbol,side=signal,amount=filled_amt,price=filled_price);filled_val=filled_amt*filled_price;portfolio_value=self._calculate_portfolio_metrics().get('current_portfolio_value',self.account_size if self.account_size>0 else 1.);frac=filled_val/portfolio_value if portfolio_value>0 else .0;decision_result['recommended_position_size']=frac
2459
+ elif signal not in['HOLD','COOLDOWN']:logger.info(f"!!! DEMO MODE: Would execute {signal} trade (live_trading_mode=False)")
2460
+ if trade_result and'pnl'in trade_result:self.circuit_breaker.record_trade(trade_result['pnl']);self.monitoring.track_pnl(timestamp=datetime.now(),pnl=trade_result['pnl'],position=portfolio_weights[0]if len(portfolio_weights)>0 else .0,price=current_price)
2461
+ self.monitoring.monitor_signal_health(signal_name='ensemble',score=confidence,decay=pred_metadata.get('prediction_std',[.0])[-1]if pred_metadata.get('prediction_std')else .0);causal_score,causal_block=self._evaluate_causal_stability();wallet_snapshot=self._get_live_wallet_snapshot();result={'signal':signal,'original_signal':original_signal if signal!=original_signal else None,'confidence':confidence,'current_price':current_price,'price_targets':price_targets,'confidence_intervals':confidence_intervals,'signal_explanations':signal_explanations,'top_signals':sorted([(k,v.get('score',.0))for(k,v)in signal_scores.items()],key=lambda x:x[1],reverse=True)[:10],'signal_contributions':pred_metadata.get('signal_contributions',{}),'risk_metrics':{'volatility':volatility,'sharpe_ratio':sharpe_ratio,'cvar_95':cvar_95,'prediction_uncertainty':latest_std},'portfolio_weight':portfolio_weights[0]if len(portfolio_weights)>0 else .0,'recommended_position_size':decision_result['recommended_position_size'],'original_position_size':original_position_size if abs(original_position_size)!=abs(decision_result['recommended_position_size'])else None,'expected_return':latest_pred,'predictions':predictions.tolist()if len(predictions)>0 else[],'regime':regime,'best_model':best_model,'entry_level':decision_result.get('entry_level',current_price),'stop_loss':decision_result.get('stop_loss',None),'take_profit':decision_result.get('take_profit',None),'trade_result':trade_result,'monitoring_summary':self.monitoring.get_summary(),'portfolio_metrics':self._calculate_portfolio_metrics(),'system_state':self.system_state,'causal_score':causal_score,'causal_block':causal_block,'wallet':wallet_snapshot}
2462
+ if not isinstance(result,dict):logger.error(f"ERROR: run() about to return {type(result)} instead of dict!");logger.error(f"Result keys: {list(result.keys())if hasattr(result,"keys")else"No keys"}");return{'error':f"Invalid return type in run(): {type(result)}"}
2463
+ self.last_full_run=result;return result
2464
+ def display_rich_results(console:Console,result:Dict[str,Any],live_mode:bool=False)->None:
2465
+ if'error'in result:console.print(Panel(f"[red]Error: {result["error"]}[/red]",title='Error',border_style='red'));return
2466
+ def _render_wallet_panel(wallet:Dict[str,Any])->None:
2467
+ if not wallet:return
2468
+ lines=[];exch=wallet.get('exchange')or'live';available=wallet.get('available_balance')
2469
+ if available is not None:lines.append(f"[bold]Available (primary):[/bold] {available:,.4f}")
2470
+ by_quote=wallet.get('by_quote',{})
2471
+ if by_quote:
2472
+ lines.append('[bold]Quotes:[/bold]')
2473
+ for(q,bal)in sorted(by_quote.items()):lines.append(f" {q}: {bal:,.4f}")
2474
+ holdings=wallet.get('holdings')or wallet.get('balances')or{}
2475
+ if holdings:
2476
+ lines.append('[bold]Assets:[/bold]')
2477
+ for(asset,bal)in sorted(holdings.items()):
2478
+ if asset in by_quote:continue
2479
+ lines.append(f" {asset}: {bal:,.6f}")
2480
+ totals=wallet.get('totals',{})
2481
+ if totals:
2482
+ lines.append('[bold]Totals:[/bold]')
2483
+ if'eur'in totals:lines.append(f" EUR total: {totals.get("eur",.0):,.4f}")
2484
+ if'usd'in totals:lines.append(f" USD total: {totals.get("usd",.0):,.4f}")
2485
+ details=wallet.get('totals',{}).get('details',[])
2486
+ if details:
2487
+ lines.append('[dim]Valuations:[/dim]')
2488
+ for d in details[:10]:lines.append(f" {d.get("asset")}: {d.get("value",0):,.4f} {d.get("quote")}")
2489
+ if exch:lines.append(f"[dim]Exchange: {exch}[/dim]")
2490
+ if lines:console.print(Panel('\n'.join(lines),title='[bold white]šŸ’° LIVE WALLET[/bold white]',border_style='white'))
2491
+ batched_results=result.get('batched_results',[]);has_multi_asset=len(batched_results)>0
2492
+ if live_mode:
2493
+ _render_wallet_panel(result.get('wallet',{}))
2494
+ if has_multi_asset:display_multi_asset_table(console,result,batched_results,live_mode=True)
2495
+ else:display_single_asset_view(console,result,live_mode=True)
2496
+ else:
2497
+ console.print();console.print(Panel.fit('[bold cyan]QUANT TRADING AGENT - MULTI-ASSET ANALYSIS[/bold cyan]',border_style='cyan',box=box.DOUBLE));console.print()
2498
+ if has_multi_asset:display_multi_asset_table(console,result,batched_results,live_mode=False)
2499
+ else:display_single_asset_view(console,result,live_mode=False)
2500
+ def display_multi_asset_table(console:Console,result:Dict[str,Any],batched_results:List[Dict],live_mode:bool=False)->None:
2501
+ all_current_prices=result.get('all_current_prices',{});all_metadata=result.get('all_metadata',{});all_intervals=result.get('all_intervals',{});price_data_dict=result.get('price_data_dict',{});portfolio_metrics=result.get('portfolio_metrics',{});positions=portfolio_metrics.get('positions',{});wallet=result.get('wallet',{})if live_mode else{}
2502
+ if live_mode:
2503
+ holdings=wallet.get('holdings')or wallet.get('balances');by_quote=wallet.get('by_quote',{})
2504
+ if holdings:
2505
+ live_positions={}
2506
+ for(asset,bal)in holdings.items():
2507
+ if asset in by_quote:continue
2508
+ if bal is None or abs(bal)==0:continue
2509
+ live_positions[asset]={'size':bal}
2510
+ positions=live_positions
2511
+ MAX_DISPLAY_ASSETS=20;total_assets=result.get('total_assets',len(all_current_prices))
2512
+ if live_mode:from datetime import datetime;timestamp=datetime.now().strftime('%H:%M:%S');table_title=f"[bold cyan]!!!LIVE ASSET TRADING DECISIONS[/bold cyan] - {timestamp}"
2513
+ else:table_title='[bold cyan]!!! ASSET TRADING DECISIONS[/bold cyan]'
2514
+ main_table=Table(title=table_title,show_header=True,header_style='bold cyan',box=box.ROUNDED,border_style='cyan',title_style='bold cyan');main_table.add_column('Asset',style='bold white',width=8);main_table.add_column('Price',justify='right',style='yellow',width=12);main_table.add_column('Signal',justify='center',width=10);main_table.add_column('Confidence',justify='right',width=12);main_table.add_column('Position',justify='right',style='green',width=12);main_table.add_column('Invested',justify='right',style='blue',width=12);main_table.add_column('PnL',justify='right',width=14);main_table.add_column('Expected Return',justify='right',width=14);main_table.add_column('Volatility',justify='right',width=12);main_table.add_column('Uncertainty',justify='right',width=12);main_table.add_column('1D Target',justify='right',style='magenta',width=12);main_table.add_column('7D Target',justify='right',style='magenta',width=12);MAX_DISPLAY_ASSETS=20;total_assets=result.get('total_assets',len(all_current_prices));all_assets_data=[]
2515
+ for batch in batched_results:
2516
+ decisions=batch.get('decisions',{})
2517
+ for(symbol,decision)in decisions.items():all_assets_data.append({'symbol':symbol,'decision':decision,'batch':batch.get('batch',0)})
2518
+ def sort_key(x):symbol=x['symbol'];decision=x['decision'];has_position=symbol in positions and positions[symbol].get('size',0)>0;confidence=decision.get('confidence',.0)if isinstance(decision,dict)else .0;signal=decision.get('signal','HOLD')if isinstance(decision,dict)else'HOLD';signal_priority={'BUY':3,'SELL':2,'COOLDOWN':1,'HOLD':1}.get(signal,0);return has_position,signal_priority,confidence,symbol
2519
+ all_assets_data.sort(key=sort_key,reverse=True);display_assets=all_assets_data[:MAX_DISPLAY_ASSETS];has_more=len(all_assets_data)>MAX_DISPLAY_ASSETS
2520
+ for asset_data in display_assets:
2521
+ symbol=asset_data['symbol'];decision=asset_data['decision'];price=all_current_prices.get(symbol,result.get('current_price',0));signal=decision.get('signal','HOLD')if isinstance(decision,dict)else'HOLD';confidence=decision.get('confidence',.0)if isinstance(decision,dict)else .0;position_size=decision.get('recommended_position_size',.0)if isinstance(decision,dict)else .0;expected_return=decision.get('expected_return',.0)if isinstance(decision,dict)else .0;metadata=all_metadata.get(symbol,{});volatility=.0;uncertainty=.0
2522
+ if isinstance(decision,dict):volatility=decision.get('volatility',.0)
2523
+ if volatility==.0 and symbol in price_data_dict:
2524
+ try:
2525
+ price_data=price_data_dict[symbol]
2526
+ if isinstance(price_data,pd.DataFrame)and'price'in price_data.columns and len(price_data)>1:
2527
+ returns=price_data['price'].pct_change().dropna()
2528
+ if len(returns)>0:volatility=float(returns.std()*np.sqrt(252))
2529
+ except Exception:pass
2530
+ if volatility==.0:volatility=.02
2531
+ if isinstance(decision,dict):uncertainty=decision.get('prediction_uncertainty',.0)
2532
+ if uncertainty==.0 and isinstance(metadata,dict):
2533
+ raw_uncertainty=metadata.get('prediction_uncertainty',.0);pred_std=metadata.get('prediction_std',[])
2534
+ if isinstance(pred_std,list)and len(pred_std)>0:
2535
+ latest_std=pred_std[-1]if len(pred_std)>0 else .0
2536
+ if expected_return!=.0 and abs(expected_return)>1e-06:uncertainty=min(2.,latest_std/abs(expected_return))
2537
+ elif latest_std>0 and price>0:uncertainty=min(2.,latest_std/price)
2538
+ else:uncertainty=.5
2539
+ elif raw_uncertainty>0:uncertainty=min(2.,raw_uncertainty)
2540
+ else:uncertainty=.5
2541
+ uncertainty=max(.01,uncertainty)
2542
+ intervals=all_intervals.get(symbol,{});target_1d=price;target_7d=price
2543
+ if intervals and isinstance(intervals,dict):
2544
+ if'1d'in intervals and isinstance(intervals['1d'],dict):target_1d=intervals['1d'].get('mean',price)
2545
+ if'7d'in intervals and isinstance(intervals['7d'],dict):target_7d=intervals['7d'].get('mean',price)
2546
+ if signal=='BUY':signal_text=Text('🟢 BUY',style='bold green')
2547
+ elif signal=='SELL':signal_text=Text('šŸ”“ SELL',style='bold red')
2548
+ elif signal=='COOLDOWN':signal_text=Text('šŸ”µ COOLDOWN',style='bold cyan')
2549
+ else:signal_text=Text('🟔 HOLD',style='bold yellow')
2550
+ if confidence>=.8:conf_style='bold green'
2551
+ elif confidence>=.6:conf_style='yellow'
2552
+ else:conf_style='red'
2553
+ if abs(position_size)>.05:pos_style='bold green'
2554
+ elif abs(position_size)>.02:pos_style='yellow'
2555
+ else:pos_style='dim'
2556
+ position_info=positions.get(symbol,{});invested_value=position_info.get('value',.0);unrealized_pnl=position_info.get('unrealized_pnl',.0);unrealized_pnl_pct=position_info.get('unrealized_pnl_pct',.0);invested_text=f"${invested_value:,.2f}"if invested_value>0 else'-'
2557
+ if unrealized_pnl>0:pnl_text=f"[green]+${unrealized_pnl:,.2f} (+{unrealized_pnl_pct:.2f}%)[/green]"
2558
+ elif unrealized_pnl<0:pnl_text=f"[red]${unrealized_pnl:,.2f} ({unrealized_pnl_pct:.2f}%)[/red]"
2559
+ else:pnl_text='-'
2560
+ main_table.add_row(symbol,f"${price:,.2f}",signal_text,f"[{conf_style}]{confidence:.1%}[/{conf_style}]",f"[{pos_style}]{position_size:.2%}[/{pos_style}]",invested_text,pnl_text,f"{expected_return:+.2%}",f"{volatility:.1%}",f"{uncertainty:.1%}",f"${target_1d:,.2f}",f"${target_7d:,.2f}")
2561
+ if has_more:remaining=len(all_assets_data)-MAX_DISPLAY_ASSETS;main_table.add_row(f"[dim]... {remaining} more[/dim]",'[dim]-[/dim]','[dim]-[/dim]','[dim]-[/dim]','[dim]-[/dim]','[dim]-[/dim]','[dim]-[/dim]','[dim]-[/dim]','[dim]-[/dim]','[dim]-[/dim]','[dim]-[/dim]','[dim]-[/dim]','[dim]-[/dim]')
2562
+ console.print(main_table)
2563
+ if total_assets>MAX_DISPLAY_ASSETS:summary_text=f"[dim]Showing top {MAX_DISPLAY_ASSETS} of {total_assets} assets (sorted by priority)[/dim]";console.print(summary_text)
2564
+ if portfolio_metrics:
2565
+ portfolio_table=Table(title='[bold green]šŸ’° PORTFOLIO SUMMARY[/bold green]',show_header=True,header_style='bold green',box=box.ROUNDED,border_style='green');portfolio_table.add_column('Metric',style='bold white',width=20);portfolio_table.add_column('Value',justify='right',style='yellow',width=20);initial_value=portfolio_metrics.get('initial_portfolio_value',.0);current_value=portfolio_metrics.get('current_portfolio_value',initial_value);baseline_value=portfolio_metrics.get('baseline_value',initial_value);total_pnl=portfolio_metrics.get('total_unrealized_pnl',.0);total_return=portfolio_metrics.get('total_return_pct',.0);baseline_return=portfolio_metrics.get('baseline_return_pct',.0);return_since_last=portfolio_metrics.get('return_since_last',.0);total_invested=portfolio_metrics.get('total_invested',.0);positions=portfolio_metrics.get('positions',{})or{}
2566
+ def _best_worst(pos_dict):
2567
+ if not pos_dict:return None,None
2568
+ sortable=[]
2569
+ for(sym,p)in pos_dict.items():pct=p.get('unrealized_pnl_pct',.0);sortable.append((pct,sym))
2570
+ if not sortable:return None,None
2571
+ sortable.sort();worst=sortable[0];best=sortable[-1];return best,worst
2572
+ best,worst=_best_worst(positions)
2573
+ if total_return>0:return_text=f"[green]+{total_return:.2f}%[/green]"
2574
+ elif total_return<0:return_text=f"[red]{total_return:.2f}%[/red]"
2575
+ else:return_text='0.00%'
2576
+ if baseline_return>0:baseline_text=f"[green]+{baseline_return:.2f}%[/green]"
2577
+ elif baseline_return<0:baseline_text=f"[red]{baseline_return:.2f}%[/red]"
2578
+ else:baseline_text='0.00%'
2579
+ def _fmt_bw(item):
2580
+ if not item:return'—'
2581
+ pct,sym=item
2582
+ if pct>0:return f"{sym}: [green]+{pct:.2f}%[/green]"
2583
+ elif pct<0:return f"{sym}: [red]{pct:.2f}%[/red]"
2584
+ return f"{sym}: 0.00%"
2585
+ if return_since_last>0:since_last_text=f"[green]+{return_since_last:.2f}%[/green]"
2586
+ elif return_since_last<0:since_last_text=f"[red]{return_since_last:.2f}%[/red]"
2587
+ else:since_last_text='0.00%'
2588
+ portfolio_table.add_row('Initial Portfolio Value',f"${initial_value:,.2f}");portfolio_table.add_row('Baseline (Ratcheted)',f"${baseline_value:,.2f}");portfolio_table.add_row('Current Portfolio Value',f"${current_value:,.2f}");portfolio_table.add_row('Total Invested',f"${total_invested:,.2f}");portfolio_table.add_row('Unrealized PnL',f"${total_pnl:+,.2f}");portfolio_table.add_row('Total Return',return_text);portfolio_table.add_row('Baseline Return',baseline_text);portfolio_table.add_row('Return Since Last Run',since_last_text);portfolio_table.add_row('Highest Gained',_fmt_bw(best));portfolio_table.add_row('Lowest Gained',_fmt_bw(worst));console.print();console.print(portfolio_table)
2589
+ causal_score=result.get('causal_score',None);causal_block=result.get('causal_block',False)
2590
+ if causal_score is not None and not live_mode:causal_text=f"[bold white]Causal score:[/bold white] {causal_score:.2f}{" 🚫 block"if causal_block else""}";console.print(Panel(causal_text,title='[bold blue]🧠 CAUSAL[/bold blue]',border_style='blue'))
2591
+ if not live_mode:
2592
+ console.print();console.print(Panel.fit('[bold cyan]!!! DETAILED ASSET ANALYSIS[/bold cyan]',border_style='cyan'));console.print();asset_panels=[]
2593
+ for asset_data in all_assets_data[:10]:
2594
+ symbol=asset_data['symbol'];decision=asset_data['decision'];info_lines=[];signal=decision.get('signal','HOLD')if isinstance(decision,dict)else'HOLD';confidence=decision.get('confidence',.0)if isinstance(decision,dict)else .0;info_lines.append(f"[bold]Signal:[/bold] {signal} ({confidence:.1%})");explanations=decision.get('explanations',[])if isinstance(decision,dict)else[]
2595
+ if explanations:
2596
+ info_lines.append('');info_lines.append('[bold]Reasons:[/bold]')
2597
+ for exp in explanations[:3]:info_lines.append(f" • {exp}")
2598
+ top_signals=decision.get('top_signals',[])if isinstance(decision,dict)else[]
2599
+ if top_signals:
2600
+ info_lines.append('');info_lines.append('[bold]Top Signals:[/bold]')
2601
+ for(sig_name,score)in top_signals[:3]:sig_short=sig_name.replace('signal_','')[:25];info_lines.append(f" {sig_short}: {score:.3f}")
2602
+ entry=decision.get('entry_level')if isinstance(decision,dict)else None;stop_loss=decision.get('stop_loss')if isinstance(decision,dict)else None;take_profit=decision.get('take_profit')if isinstance(decision,dict)else None
2603
+ if entry or stop_loss or take_profit:
2604
+ info_lines.append('');info_lines.append('[bold]Levels:[/bold]')
2605
+ if entry:info_lines.append(f" Entry: ${entry:,.2f}")
2606
+ if stop_loss:info_lines.append(f" Stop: ${stop_loss:,.2f}")
2607
+ if take_profit:info_lines.append(f" Target: ${take_profit:,.2f}")
2608
+ causal_score=decision.get('causal_score')if isinstance(decision,dict)else None;causal_block=decision.get('causal_block',False)if isinstance(decision,dict)else False
2609
+ if causal_score is not None:info_lines.append('');info_lines.append('[bold]Causal:[/bold]');info_lines.append(f" Score: {causal_score:.2f}{" 🚫 block"if causal_block else""}")
2610
+ panel_content='\n'.join(info_lines)if info_lines else'No detailed data available';border_color='green'if signal=='BUY'else'red'if signal=='SELL'else'cyan'if signal=='COOLDOWN'else'yellow';asset_panels.append(Panel(panel_content,title=f"[bold]{symbol}[/bold]",border_style=border_color,box=box.ROUNDED))
2611
+ if asset_panels:console.print(Columns(asset_panels,equal=True,expand=True));console.print()
2612
+ if not live_mode:display_summary_stats(console,result,all_assets_data)
2613
+ def display_single_asset_view(console:Console,result:Dict[str,Any],live_mode:bool=False)->None:
2614
+ if live_mode:from datetime import datetime;timestamp=datetime.now().strftime('%H:%M:%S');signal=result.get('signal','HOLD');confidence=result.get('confidence',.0);price=result.get('current_price',0);signal_emoji='🟢'if signal=='BUY'else'šŸ”“'if signal=='SELL'else'šŸ”µ'if signal=='COOLDOWN'else'🟔';live_table=Table(title=f"[bold cyan]šŸ“Š LIVE TRADING DECISION[/bold cyan] - {timestamp}",show_header=True,box=box.ROUNDED);live_table.add_column('Metric',style='bold white');live_table.add_column('Value',justify='right',style='yellow');live_table.add_row('Signal',f"{signal_emoji} {signal}");live_table.add_row('Confidence',f"{confidence:.1%}");live_table.add_row('Price',f"${price:,.2f}");live_table.add_row('Expected Return',f"{result.get("expected_return",0):+.2%}");live_table.add_row('Position Size',f"{result.get("recommended_position_size",0):.2%}");console.print(live_table);return
2615
+ signal=result.get('signal','HOLD');confidence=result.get('confidence',.0);price=result.get('current_price',0);signal_color='green'if signal=='BUY'else'red'if signal=='SELL'else'cyan'if signal=='COOLDOWN'else'white'if signal=='EXIT'else'yellow';signal_emoji='🟢'if signal=='BUY'else'šŸ”“'if signal=='SELL'else'šŸ”µ'if signal=='COOLDOWN'else'⚪'if signal=='EXIT'else'🟔';decision_text=f"""
2616
+ [bold white]Signal:[/bold white] [{signal_color}]{signal_emoji} {signal}[/{signal_color}] ({confidence:.1%})
2617
+ [bold white]Price:[/bold white] ${price:,.2f}
2618
+ [bold white]Position Size:[/bold white] {result.get("recommended_position_size",0):.2%}
2619
+ [bold white]Expected Return:[/bold white] {result.get("expected_return",0):+.2%}
2620
+ [bold white]Regime:[/bold white] {result.get("regime","unknown")}
2621
+ """;console.print(Panel(decision_text.strip(),title='[bold cyan]šŸŽÆ TRADING DECISION[/bold cyan]',border_style='cyan'));console.print();targets=result.get('price_targets',{});intervals=result.get('confidence_intervals',{})
2622
+ if targets or intervals:
2623
+ targets_table=Table(show_header=True,header_style='bold magenta',box=box.SIMPLE);targets_table.add_column('Horizon',style='cyan');targets_table.add_column('Target Price',justify='right',style='yellow');targets_table.add_column('95% CI Lower',justify='right');targets_table.add_column('95% CI Upper',justify='right')
2624
+ for horizon in['1d','3d','7d']:
2625
+ if horizon in targets and horizon in intervals:ci=intervals[horizon];targets_table.add_row(horizon.upper(),f"${targets[horizon]:,.2f}",f"${ci.get("lower",0):,.2f}",f"${ci.get("upper",0):,.2f}")
2626
+ console.print(Panel(targets_table,title='[bold magenta]šŸ’° PRICE TARGETS[/bold magenta]',border_style='magenta'));console.print()
2627
+ risk=result.get('risk_metrics',{})
2628
+ if risk:risk_text=f"""
2629
+ [bold white]Volatility:[/bold white] {risk.get("volatility",0):.2%}
2630
+ [bold white]Sharpe Ratio:[/bold white] {risk.get("sharpe_ratio",0):.2f}
2631
+ [bold white]CVaR (95%):[/bold white] {risk.get("cvar_95",0):.2%}
2632
+ [bold white]Uncertainty:[/bold white] {risk.get("prediction_uncertainty",0):.2%}
2633
+ """;console.print(Panel(risk_text.strip(),title='[bold red]āš–ļø RISK METRICS[/bold red]',border_style='red'));console.print()
2634
+ causal_score=result.get('causal_score',None);causal_block=result.get('causal_block',False)
2635
+ if causal_score is not None:causal_text=f"[bold white]Causal:[/bold white] {causal_score:.2f}{" 🚫 block"if causal_block else""}";console.print(Panel(causal_text,title='[bold blue]🧠 CAUSAL[/bold blue]',border_style='blue'));console.print()
2636
+ explanations=result.get('signal_explanations',[])
2637
+ if explanations:exp_text='\n'.join([f" • {exp}"for exp in explanations[:8]]);console.print(Panel(exp_text,title='[bold yellow]šŸ” SIGNAL EXPLANATIONS[/bold yellow]',border_style='yellow'))
2638
+ def display_summary_stats(console:Console,result:Dict[str,Any],all_assets_data:List[Dict])->None:buy_count=sum(1 for a in all_assets_data if isinstance(a['decision'],dict)and a['decision'].get('signal')=='BUY');sell_count=sum(1 for a in all_assets_data if isinstance(a['decision'],dict)and a['decision'].get('signal')=='SELL');hold_count=sum(1 for a in all_assets_data if isinstance(a['decision'],dict)and a['decision'].get('signal')=='HOLD');cooldown_count=sum(1 for a in all_assets_data if isinstance(a['decision'],dict)and a['decision'].get('signal')=='COOLDOWN');total_assets=len(all_assets_data);total_trades=result.get('total_trades',0);summary_text=f"""
2639
+ [bold white]Total Assets Analyzed:[/bold white] {total_assets}
2640
+ [bold green]BUY Signals:[/bold green] {buy_count}
2641
+ [bold red]SELL Signals:[/bold red] {sell_count}
2642
+ [bold yellow]HOLD Signals:[/bold yellow] {hold_count}
2643
+ [bold cyan]COOLDOWN Signals:[/bold cyan] {cooldown_count}
2644
+ [bold cyan]Trades Executed:[/bold cyan] {total_trades}
2645
+ """;console.print(Panel(summary_text.strip(),title='[bold cyan]šŸ“Š SUMMARY STATISTICS[/bold cyan]',border_style='cyan'));console.print()
2646
+ def display_simple_results(result:Dict[str,Any])->None:
2647
+ print('\n'+'='*80);print('QUANT TRADING AGENT - DETAILED ANALYSIS');print('='*80)
2648
+ if'error'in result:print(f"Error: {result["error"]}");return
2649
+ print(f"\nšŸ“Š CURRENT MARKET STATE");print(f" Current Price: ${result.get("current_price",0):,.2f}");print(f" Market Regime: {result.get("regime","unknown")}");print(f" Volatility: {result.get("risk_metrics",{}).get("volatility",0):.2%}");print(f"\nšŸŽÆ TRADING DECISION");signal=result.get('signal','HOLD');confidence=result.get('confidence',.0);print(f" Signal: {signal} (Confidence: {confidence:.0%})");print(f" Expected Return: {result.get("expected_return",0):.2%}");print(f" Recommended Position Size: {result.get("recommended_position_size",0):.2%}");batched_results=result.get('batched_results',[])
2650
+ if batched_results:
2651
+ print(f"\nšŸ“Š MULTI-ASSET RESULTS")
2652
+ for batch in batched_results:
2653
+ decisions=batch.get('decisions',{})
2654
+ for(symbol,decision)in decisions.items():sig=decision.get('signal','HOLD')if isinstance(decision,dict)else'HOLD';conf=decision.get('confidence',.0)if isinstance(decision,dict)else .0;price=decision.get('current_price',result.get('current_price',0))if isinstance(decision,dict)else result.get('current_price',0);print(f" {symbol}: {sig} ({conf:.1%}) @ ${price:,.2f}")
2655
+ async def main()->None:
2656
+ import sys;use_streaming='--stream'in sys.argv or os.getenv('STREAMING_MODE','false').lower()=='true';longterm_mode='--longterm-mode'in sys.argv or os.getenv('LONGTERM_MODE','').lower()=='true'or LONGTERM_MODE_ENABLED;agent=QuantTradingAgent(days_back=DAYS_BACK,demo_mode=not LIVE_TRADING_MODE_DEFAULT,live_trading_mode=LIVE_TRADING_MODE_DEFAULT,longterm_mode=longterm_mode)
2657
+ if use_streaming:
2658
+ print('Starting real-time streaming mode...');print('Press Ctrl+C to stop')
2659
+ async def on_signal_update(agent_instance):
2660
+ try:
2661
+ result=await agent_instance.run()
2662
+ if result and'error'not in result:signal,price,confidence=result.get('signal','HOLD'),result.get('current_price',0),result.get('confidence',0);print(f"\n[{datetime.now().strftime("%H:%M:%S")}] Signal: {signal} | Price: ${price:.2f} | Confidence: {confidence:.0%}")
2663
+ except Exception as e:logger.debug(f"Error in streaming callback: {e}")
2664
+ await agent.start_streaming(exchange='coinbase',symbol='ETH',decision_callback=on_signal_update)
2665
+ try:
2666
+ while agent.streaming_mode:await asyncio.sleep(1)
2667
+ except KeyboardInterrupt:print('\nStopping streaming...');await agent.stop_streaming('coinbase','ETH')
2668
+ else:
2669
+ console=Console()if RICH_AVAILABLE else None;last_result=None
2670
+ if longterm_mode:position_eval_interval=getattr(agent,'position_evaluation_interval_hours',1)*3600;full_refresh_hours=getattr(agent,'full_refresh_interval_hours',24);display_update_interval=position_eval_interval;full_refresh_interval=full_refresh_hours*3600;mode_text=f"[bold cyan]šŸ” LONGTERM MODE[/bold cyan]\n[dim]Position evaluation: every {position_eval_interval/3600:.1f} hour(s)\nFull refresh: every {full_refresh_hours} hour(s)\nMax position size: {getattr(agent,"max_position_size",.005):.2%}\nMin confidence: {getattr(agent,"min_confidence_threshold",.85):.0%}[/dim]"
2671
+ else:display_update_interval,full_refresh_interval=1.,3e2;mode_text='[dim]Full refresh every 5 min, display updates every 1 sec[/dim]'
2672
+ last_full_refresh,first_run=.0,True
2673
+ try:
2674
+ if console:title='[bold green]šŸš€ QUANT TRADING AGENT - LIVE MODE[/bold green]'if not longterm_mode else'[bold cyan]šŸ” QUANT TRADING AGENT - LONGTERM MODE[/bold cyan]';console.print(Panel.fit(f"{title}\n[yellow]Press Ctrl+C to stop and view detailed analysis[/yellow]\n{mode_text}",border_style='green'if not longterm_mode else'cyan',box=box.DOUBLE));console.print()
2675
+ else:
2676
+ mode_title='šŸš€ QUANT TRADING AGENT - LIVE MODE'if not longterm_mode else'šŸ” QUANT TRADING AGENT - LONGTERM MODE';print(mode_title);print('Press Ctrl+C to stop and view detailed analysis')
2677
+ if longterm_mode:print(f"Position evaluation: every {position_eval_interval/3600:.1f} hour(s)");print(f"Full refresh: every {full_refresh_hours} hour(s)")
2678
+ else:print('Full refresh every 5 min, display updates every 1 sec')
2679
+ print()
2680
+ import time
2681
+ while True:
2682
+ try:
2683
+ current_time=time.time();time_since_full_refresh=current_time-last_full_refresh
2684
+ if first_run or time_since_full_refresh>=full_refresh_interval:
2685
+ if console and not first_run:console.print('[dim]Performing full refresh...[/dim]')
2686
+ result=await agent.run(incremental=False)
2687
+ if not isinstance(result,dict):logger.error(f"ERROR: run() returned {type(result)} instead of dict!");logger.error(f"Result: {result}");result={'error':f"Invalid return type: {type(result)}"}
2688
+ last_result,last_full_refresh,first_run=result,current_time,False
2689
+ else:result=await agent.run(incremental=True);last_result=result
2690
+ if not isinstance(result,dict):logger.error(f"ERROR: run() returned {type(result)} instead of dict!");logger.error(f"Result: {result}");result={'error':f"Invalid return type: {type(result)}"}
2691
+ if'error'in result:
2692
+ if console:console.print(f"[red]Error: {result["error"]}[/red]")
2693
+ else:print(f"Error: {result["error"]}")
2694
+ await asyncio.sleep(display_update_interval);continue
2695
+ if console:console.clear();display_rich_results(console,result,live_mode=True)
2696
+ else:print(f"\n[{datetime.now().strftime("%H:%M:%S")}] Analysis complete");display_simple_results(result)
2697
+ sleep_time=display_update_interval*(agent.cooldown_sleep_multiplier if agent.system_state=='COOLDOWN'else 1.);await asyncio.sleep(sleep_time)
2698
+ except Exception as e:
2699
+ import traceback;error_msg=str(e)
2700
+ if'DataFrame'in error_msg and'ambiguous'in error_msg:
2701
+ logger.error(f"DataFrame boolean ambiguity error detected!");logger.error(f"Result type: {type(result)}")
2702
+ if hasattr(result,'shape'):logger.error(f"Result shape: {result.shape}")
2703
+ if hasattr(result,'columns'):logger.error(f"Result columns: {list(result.columns)}")
2704
+ logger.error(f"Last few calls in stack:")
2705
+ for line in traceback.format_exc().split('\n')[-10:]:
2706
+ if line.strip():logger.error(f" {line}")
2707
+ else:logger.error(f"Error in live mode: {e}");logger.error(f"Traceback: {traceback.format_exc()}")
2708
+ await asyncio.sleep(display_update_interval);continue
2709
+ except KeyboardInterrupt:
2710
+ if console:console.print('\n');console.print(Panel.fit('[bold yellow]šŸ“Š FINAL DETAILED ANALYSIS[/bold yellow]',border_style='yellow',box=box.DOUBLE));console.print()
2711
+ try:await agent._exit_liquidate_all()
2712
+ except Exception as e:logger.warning(f"[EXIT] Liquidation on exit failed: {e}")
2713
+ if last_result:
2714
+ last_result=dict(last_result);last_result['signal']='EXIT'
2715
+ if RICH_AVAILABLE and console:display_rich_results(console,last_result,live_mode=False)
2716
+ else:display_simple_results(last_result)
2717
+ signal,confidence=last_result.get('signal','HOLD'),last_result.get('confidence',.0);logger.info(f"Agent run completed: {signal} (confidence: {confidence:.0%})")
2718
+ elif console:console.print('[yellow]No analysis data available[/yellow]')
2719
+ else:print('No analysis data available')
2720
+ if __name__=='__main__':
2721
+ import sys;longterm_mode='--longterm-mode'in sys.argv or os.getenv('LONGTERM_MODE','').lower()=='true'or LONGTERM_MODE_ENABLED;print('='*80)
2722
+ if longterm_mode:print('šŸ” QUANT TRADING AGENT - LONGTERM MODE - STARTING');print('='*80);print('šŸ“Š LONGTERM MODE ACTIVE:');print(' - Slow, precise long-term investment system');print(' - Predictions for next day/week instead of seconds');print(' - Position evaluation: hourly (trades only with extremely high confidence)');print(' - Prioritizes assets valued less than 1 USD/EUR');print(' - Maximum position size: 0.5% of portfolio');print(' - Minimum confidence threshold: 85%');print(' - Designed for background server operation');print('='*80)
2723
+ else:print('!!! QUANT TRADING AGENT - STARTING');print('='*80)
2724
+ print('!!! SAFETY MODE: Demo mode enabled by default');print(' - NO real trades will be executed');print(' - Only analysis and simulation will run');print(' - To enable live trading, set live_trading_mode=True explicitly')
2725
+ if not longterm_mode:print(' - To enable longterm mode, use --longterm-mode flag or set LONGTERM_MODE=true')
2726
+ print('='*80);print()
2727
+ try:asyncio.run(main())
2728
+ except KeyboardInterrupt:print('\n!!! Stopping agent...')