{"id":654,"date":"2021-09-21T22:08:11","date_gmt":"2021-09-21T22:08:11","guid":{"rendered":"https:\/\/jeremywhittaker.com\/?page_id=654"},"modified":"2026-03-21T12:47:38","modified_gmt":"2026-03-21T19:47:38","slug":"homepage","status":"publish","type":"page","link":"https:\/\/new.jeremywhittaker.com\/","title":{"rendered":"Jeremy Whittaker"},"content":{"rendered":"<ul class=\"wp-block-latest-posts__list has-dates has-author alignleft wp-block-latest-posts\"><li><a class=\"wp-block-latest-posts__post-title\" href=\"https:\/\/new.jeremywhittaker.com\/index.php\/2026\/03\/21\/the-libertarian-case-for-decentralized-energy-and-my-votes-in-the-srp-election\/\">The Libertarian Case for Decentralized Energy \u2014 and My Votes in the SRP Election<\/a><div class=\"wp-block-latest-posts__post-author\">by JeremyWhittaker<\/div><time datetime=\"2026-03-21T11:29:39-07:00\" class=\"wp-block-latest-posts__post-date\">March 21, 2026<\/time><div class=\"wp-block-latest-posts__post-full-content\"><!-- wp:paragraph -->\n<p>For me solar and electric cars have always been mainly about independence. <strong>Independence from big government, corporations, and foreign wars. <\/strong><\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":12351,\"sizeSlug\":\"large\",\"linkDestination\":\"none\",\"align\":\"right\"} -->\n<figure class=\"wp-block-image alignright size-large\"><img decoding=\"async\" width=\"1024\" height=\"683\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/energy-independence-1024x683.png\" alt=\"\" class=\"wp-image-12351 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/energy-independence-1024x683.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/energy-independence-300x200.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/energy-independence-768x512.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/energy-independence-450x300.png 450w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/energy-independence.png 1536w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/683;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:paragraph -->\n<p>Over my entire adult life I have driven hybrids and electric vehicles, built my own battery systems, installed multiple solar panel systems, and experimented with SRP time-of-use pricing. I did this because<strong> I do not want my family\u2019s transportation and home energy tied to foreign wars in the Middle East, oil markets, imported crude, or big corporate interests. <\/strong>Transportation is still where most petroleum gets burned in this country, and every mile I move from gasoline to electricity is one less mile dependent on this fragile system.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":12341,\"sizeSlug\":\"large\",\"linkDestination\":\"none\",\"align\":\"left\"} -->\n<figure class=\"wp-block-image alignleft size-large\"><img decoding=\"async\" width=\"1024\" height=\"552\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/dji_fly_20250521_154810_424_1747867715043_photo-1024x552.jpg\" alt=\"\" class=\"wp-image-12341 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/dji_fly_20250521_154810_424_1747867715043_photo-1024x552.jpg 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/dji_fly_20250521_154810_424_1747867715043_photo-300x162.jpg 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/dji_fly_20250521_154810_424_1747867715043_photo-768x414.jpg 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/dji_fly_20250521_154810_424_1747867715043_photo-1536x828.jpg 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/dji_fly_20250521_154810_424_1747867715043_photo-2048x1104.jpg 2048w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/dji_fly_20250521_154810_424_1747867715043_photo-500x270.jpg 500w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/552;\" \/><figcaption class=\"wp-element-caption\">My latest 23kW solar system with 26kWh of batteries<\/figcaption><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>The problem with centralized systems.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>In Arizona, you do not have a choice where you get your electricity from. <strong>You do not shop for your utility. You do not compare rates. You do not negotiate anything. You are stuck with a provider based on where you live, and then you live inside whatever structure they have built.<\/strong> And systems like this trap people into a broken system that does not benefit them.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>What small government should mean.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This is where I think a lot of people misunderstand what \u201csmall government\u201d should actually mean. If the goal is less dependence on large institutions, then energy is one of the clearest places to apply that thinking. A household that can produce some of its own power, store it, and decide when to use the grid is in a very different position than one that is fully dependent on it. And if that same household is driving on electricity instead of gasoline, it has also moved a major part of daily life out of the oil market. That does not mean disconnecting from the grid. It means you are no longer entirely at its mercy. To me, that is the more genuinely libertarian direction: less captivity, more optionality, and more control pushed back toward the homeowner. Petroleum still dominates transportation, while oil is only a tiny part of how electricity is generated in the United States. That is exactly why electrification matters.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>Why this is becoming practical now.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>For a long time, that idea was hard to implement in the real world. Solar was expensive. Batteries did not make sense for most people. EVs were niche. That has changed. The technology has matured enough that this is now something normal households can realistically do. The only real missing piece used to be storage. Solar during the day is easy. Nighttime is the problem. That gap is closing fast. Battery systems are improving, getting cheaper, and becoming realistic at the home level. I have built and tested enough of this to be confident it is not theoretical anymore.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>Why global instability still matters here at home.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>At the same time, the risks tied to centralized energy have not gone away. If anything, they have become more obvious. You do not have to look far. The situation involving Iran is a good example. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":12345,\"sizeSlug\":\"full\",\"linkDestination\":\"none\",\"align\":\"center\"} -->\n<figure class=\"wp-block-image aligncenter size-full\"><img decoding=\"async\" width=\"825\" height=\"457\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/image-1.png\" alt=\"\" class=\"wp-image-12345 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/image-1.png 825w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/image-1-300x166.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/image-1-768x425.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/image-1-500x277.png 500w\" data-sizes=\"(max-width: 825px) 100vw, 825px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 825px; --smush-placeholder-aspect-ratio: 825\/457;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:paragraph -->\n<p>A significant amount of global oil still moves through a single chokepoint, and when that becomes unstable, prices react immediately. That pressure does not stay contained to one region. It flows through markets, into inflation, and eventually into the cost of living here at home.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":12339,\"sizeSlug\":\"full\",\"linkDestination\":\"none\",\"align\":\"right\"} -->\n<figure class=\"wp-block-image alignright size-full\"><img decoding=\"async\" width=\"660\" height=\"386\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/image.png\" alt=\"\" class=\"wp-image-12339 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/image.png 660w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/image-300x175.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/image-500x292.png 500w\" data-sizes=\"(max-width: 660px) 100vw, 660px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 660px; --smush-placeholder-aspect-ratio: 660\/386;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:paragraph -->\n<p>But this is exactly why I care so much about EVs, solar, and batteries. Transportation is still overwhelmingly tied to petroleum in the United States. Electricity is not. EIA says petroleum products made up about 89% of U.S. transportation energy use in 2023, while petroleum supplied only about 0.6% of U.S. electricity generation in 2024. So when I move my family from gasoline to electricity, I am not just changing fuels. I am reducing our exposure to the global oil market itself. And when that electricity can come from my own roof or from stored power in my garage, I reduce that exposure even more. That is what decentralized energy means in practical terms. It means foreign oil shocks matter less to your household than they otherwise would.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>Why the SRP election matters.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Which brings this back to SRP. SRP is different from APS in one very important respect: SRP is a political subdivision of Arizona, its prices are not regulated by the Arizona Corporation Commission, and SRP\u2019s publicly elected Board of Directors has the authority to establish electric prices. <strong>Ballots for this election are mailed March 11, the final day to <a href=\"https:\/\/www.srpnet.com\/about\/governance-leadership\/elections\/early-voting-ballot-request-form\">request one<\/a> is March 27, and election day is April 7<\/strong>. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:gallery {\"linkTo\":\"none\"} -->\n<figure class=\"wp-block-gallery has-nested-images columns-default is-cropped\"><!-- wp:image {\"id\":12358,\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" width=\"797\" height=\"1024\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163958825.MP_-1-797x1024.jpg\" alt=\"\" class=\"wp-image-12358 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163958825.MP_-1-797x1024.jpg 797w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163958825.MP_-1-233x300.jpg 233w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163958825.MP_-1-768x987.jpg 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163958825.MP_-1-1195x1536.jpg 1195w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163958825.MP_-1-1593x2048.jpg 1593w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163958825.MP_-1-scaled.jpg 1992w\" data-sizes=\"(max-width: 797px) 100vw, 797px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 797px; --smush-placeholder-aspect-ratio: 797\/1024;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":12357,\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" width=\"814\" height=\"1024\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_164003215.MP_-1-814x1024.jpg\" alt=\"\" class=\"wp-image-12357 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_164003215.MP_-1-814x1024.jpg 814w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_164003215.MP_-1-239x300.jpg 239w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_164003215.MP_-1-768x966.jpg 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_164003215.MP_-1-1222x1536.jpg 1222w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_164003215.MP_-1-1629x2048.jpg 1629w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_164003215.MP_-1-scaled.jpg 2036w\" data-sizes=\"(max-width: 814px) 100vw, 814px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 814px; --smush-placeholder-aspect-ratio: 814\/1024;\" \/><\/figure>\n<!-- \/wp:image --><\/figure>\n<!-- \/wp:gallery -->\n\n<!-- wp:paragraph -->\n<p>This is not a ceremonial race. It directly affects pricing and policy for the system. Because if Arizona is going to move more transportation off gasoline and onto electricity, then the rules governing rooftop solar, storage, and home charging matter a great deal \u2014 and SRP\u2019s board has direct influence over those rules.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>The direction of the race.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>There are two clear slates in this election. The SRP Clean Energy Team is running on affordability, distributed energy, water protection, and making data centers pay their fair share. The opposing slate is the one drawing support from the political and business establishment. Turning Point Action has made its backing explicit, and because it is a 501(c)(4), the public generally does not get a normal public-facing donor list through IRS disclosure. Alongside it is Arizonans for Responsible Growth, a business-aligned PAC backing the same slate and openly supported by large-dollar contributors in the public record, including corporate and construction interests. <strong>Voters do not need to assume a conspiracy to notice the obvious: organized money and organized power are flooding this election with money.<\/strong><\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>Why 2029 matters more than most people realize.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The real fight over rooftop solar in SRP territory is not some abstract policy debate. It has a date on it: November 2029. That is when SRP\u2019s remaining net-metering plans disappear. SRP\u2019s own ratebook says the E-27 Customer Generation plan and the E-15 Average Demand plan will be eliminated in the November 2029 billing cycle, and customers still on those plans will be moved onto newer structures like E-16. That matters because SRP\u2019s own comparison page shows E-27 and E-15 are the plans that still offer net metering. After 2029, that feature is effectively gone.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>What replaces it is the real story.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The replacement model is not net metering. It is export-rate pricing. Under SRP\u2019s export-based plans, what you take from the grid and what you send back are tracked separately, and exported solar is credited at just 3.45 cents per kWh. At the same time, SRP has shifted its newer rate plans so that 8 a.m. to 3 p.m. \u2014 the exact window when rooftop solar produces best \u2014 is the cheap part of the day, while evening hours remain the expensive period. SRP says this reflects abundant low-cost utility-scale solar on its system. The practical result is obvious: homeowners are pushed to sell their daytime solar cheaply and buy power back later at much higher rates.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>Why that can cripple rooftop solar economics.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Rooftop solar works best for ordinary families when daytime production meaningfully offsets what they would otherwise pay the utility. SRP is moving away from that model. The only plans it still identifies as net-metering plans are going away in 2029, and the replacement structures value exported power far less generously. Outside solar advocates say SRP\u2019s policies already discourage solar adoption, and even local installers are warning that the new structure is a direct hit to the economics of residential solar. So no, SRP is not outlawing rooftop solar. But by eliminating net metering and replacing it with low export credits and evening-heavy peak pricing, it is setting up a rate design that can make rooftop solar far less attractive for average homeowners.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>Why that should concern even non-solar households.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Some people will hear that and think this is only a rooftop-solar complaint. It is not. The larger question is who the system is being designed around. The next phase of demand growth in Arizona is being driven by large, constant loads, especially data centers. SRP says data centers contributed 5.1% of its summer 2025 peak demand record and are projected to be its fastest-growing customer segment. That means cost allocation, grid planning, and price design are only going to get more important from here. If the board is not careful, ordinary households can end up carrying more of the burden while large new loads reshape the system around themselves.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>Why organized money is showing up.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>That is also why you are seeing organized campaigns, endorsements, and political energy around an election most people barely know exists. That does not happen by accident. When outside effort shows up in a low-visibility race, it usually means the stakes are higher than people think. The prudent response is not to shrug. It is to ask what direction these people are trying to lock in before the public notices.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>My votes in the SRP-wide races.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>For me, the question comes down to direction. Do we keep reinforcing a system where everything stays centralized and the individual remains fully dependent? Or do we start moving toward something more balanced, where households have more flexibility, more transparency, and more ability to manage their own energy? For that reason, in the SRP-wide races, I am voting for the Clean Energy Team: Sandra Kennedy, Casey Clowes, Krista O\u2019Brien, and Kathy Mohr-Almeida. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"lightbox\":{\"enabled\":false},\"id\":12344,\"sizeSlug\":\"large\",\"linkDestination\":\"custom\",\"align\":\"center\"} -->\n<figure class=\"wp-block-image aligncenter size-large\"><a href=\"https:\/\/srpcleanenergy.org\/\"><img decoding=\"async\" width=\"1024\" height=\"563\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163954092.MP_-1-1024x563.jpg\" alt=\"\" class=\"wp-image-12344 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163954092.MP_-1-1024x563.jpg 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163954092.MP_-1-300x165.jpg 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163954092.MP_-1-768x422.jpg 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163954092.MP_-1-1536x844.jpg 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163954092.MP_-1-2048x1125.jpg 2048w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2026\/03\/PXL_20260321_163954092.MP_-1-500x275.jpg 500w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/563;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:paragraph -->\n<p>Their <a href=\"https:\/\/srpcleanenergy.org\/\">site <\/a>frames the race around affordability, renewable energy, water protection, and making data centers pay their fair share. Between the two directions available, they are plainly closer to the one that gives more leverage back to the individual instead of concentrating it further.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>The future that is already visible.<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The future of energy in Arizona is already visible if you step back far enough. Solar on the roof. Storage in the garage. An EV in the driveway. A grid that still exists, but is no longer the only option. Not independence in the absolute sense. But less dependence. More control. And just as important, less exposure to gasoline prices, imported oil, and foreign instability. If transportation still runs mainly on petroleum, then moving transportation onto electricity is one of the clearest ways to weaken that hold over time. That is a better system. And we are close enough now that it is worth paying attention to which direction we push it.<\/p>\n<!-- \/wp:paragraph --><\/div><\/li>\n<li><a class=\"wp-block-latest-posts__post-title\" href=\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/12\/03\/youtube-videos-to-mp3-and-transcription\/\">YouTube Videos to MP3 and Transcription<\/a><div class=\"wp-block-latest-posts__post-author\">by JeremyWhittaker<\/div><time datetime=\"2024-12-03T10:28:24-07:00\" class=\"wp-block-latest-posts__post-date\">December 3, 2024<\/time><div class=\"wp-block-latest-posts__post-full-content\"><!-- wp:paragraph -->\n<p>I find myself listening to videos on YouTube quite frequently where it would be nice to dump them into an MP3 and take them on the road. This script will do exactly that. I also added transcription for analysis. Just put in your link and it will generate the MP3 and TXT file. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:html -->\n\n<!-- \/wp:html --><\/div><\/li>\n<li><a class=\"wp-block-latest-posts__post-title\" href=\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/09\/24\/analyzing-any-polymarket-users-trades-using-polygon\/\">Analyzing Any Polymarket User&#8217;s Trades Using Polygon<\/a><div class=\"wp-block-latest-posts__post-author\">by JeremyWhittaker<\/div><time datetime=\"2024-09-24T13:42:44-07:00\" class=\"wp-block-latest-posts__post-date\">September 24, 2024<\/time><div class=\"wp-block-latest-posts__post-full-content\"><!-- wp:paragraph -->\n<p><a href=\"https:\/\/Polymarket.com\">Polymarket.com<\/a>, a prediction market platform, operates on the Ethereum blockchain through the Polygon network, making it possible to analyze user transactions directly from the blockchain. By accessing a user\u2019s wallet address, we can examine their trades in detail, track profit\/loss, and monitor position changes over time. In this post, I&#8217;ll show how you can leverage the Polygon blockchain data to analyze trades on Polymarket using wallet IDs and some helpful Python code.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>Below are examples of the kinds of charts the script generates and their significance.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p><strong>Shares by Market<\/strong><\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>This chart focuses on the number of shares the user holds across different markets. It\u2019s another way to visualize their exposure to various outcomes but focuses on the number of shares rather than their total purchase value.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p><strong>Insight<\/strong>: Larger bars suggest higher exposure to specific markets. The average price paid per share is also annotated, providing further context.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p><strong>Purpose<\/strong>: Useful for understanding the user&#8217;s position size in each market.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11918,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-42.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-42-1024x771.png\" alt=\"\" class=\"wp-image-11918 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-42-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-42-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-42-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-42-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-42-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-42.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\"><strong>Total Purchase Value by Market<\/strong><\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>In this bar chart, the user\u2019s total purchase value is broken down by market. The height of each bar indicates how much the user has invested in each specific market.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\"><!-- wp:list-item -->\n<li><strong>Purpose<\/strong>: It allows for a clear visualization of where the user is concentrating their funds, showing which markets hold the largest portion of their portfolio.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Insight<\/strong>: The accompanying labels provide information about the average price paid per share in each market, helping understand whether the user is buying low or high within a market.<\/li>\n<!-- \/wp:list-item --><\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:image {\"id\":11920,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-41.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-41-1024x771.png\" alt=\"\" class=\"wp-image-11920 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-41-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-41-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-41-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-41-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-41-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-41.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11919,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-40.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-40-1024x771.png\" alt=\"\" class=\"wp-image-11919 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-40-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-40-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-40-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-40-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-40-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-40.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\"><strong>Total Purchase Value Timeline<\/strong><\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This scatter plot shows the timeline of the user\u2019s trades by plotting the total purchase value of trades over time.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\"><!-- wp:list-item -->\n<li><strong>Purpose<\/strong>: This chart reveals when the user made their largest investments, showing the fluctuations in purchase value across trades.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Insight<\/strong>: Each dot represents a trade, with its position on the Y-axis showing the value and on the X-axis showing the timestamp of the transaction. You can use this chart to understand when the user made big moves in the market.<\/li>\n<!-- \/wp:list-item --><\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:image {\"id\":11921,\"width\":\"614px\",\"height\":\"auto\",\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large is-resized\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-39.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-39-1024x771.png\" alt=\"\" class=\"wp-image-11921 lazyload\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;width:614px;height:auto\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-39-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-39-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-39-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-39-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-39-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-39.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\"><strong>Holdings by Market and Outcome (Treemap)<\/strong><\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The treemap provides a more detailed look at the user&#8217;s holdings, breaking down their positions by both market and outcome. Each rectangle represents the shares held in a particular market-outcome pair, with the size of the rectangle proportional to the user&#8217;s investment.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\"><!-- wp:list-item -->\n<li><strong>Purpose<\/strong>: Ideal for visually assessing how much the user has allocated to each market and outcome combination.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Insight<\/strong>: It highlights not just which markets the user has invested in but also how they&#8217;ve distributed their bets across different outcomes within those markets.<\/li>\n<!-- \/wp:list-item --><\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:image {\"id\":11922,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-38.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-38-1024x771.png\" alt=\"\" class=\"wp-image-11922 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-38-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-38-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-38-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-38-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-38-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-38.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\"><strong>Holdings Distribution by Market (Pie Chart)<\/strong><\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This pie chart visualizes the user\u2019s current holdings, showing the percentage distribution of shares across different markets.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\"><!-- wp:list-item -->\n<li><strong>Purpose<\/strong>: Offers a high-level overview of the user&#8217;s portfolio diversification across different markets.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Insight<\/strong>: Larger slices indicate heavier investments in specific markets, allowing you to quickly see where the user has concentrated their bets.<\/li>\n<!-- \/wp:list-item --><\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:image {\"id\":11923,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-37.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-37-1024x771.png\" alt=\"\" class=\"wp-image-11923 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-37-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-37-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-37-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-37-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-37-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-37.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\"><strong>Cumulative Shares Over Time<\/strong><\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This line chart tracks the cumulative number of shares held by the user in various markets over time. It helps visualize when the user bought or sold shares and how their position has evolved in each market.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\"><!-- wp:list-item -->\n<li><strong>Purpose<\/strong>: This chart is essential for understanding the user\u2019s trading strategy over time, revealing periods of heavy buying or selling.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Insight<\/strong>: Each line represents a different market and outcome combination. Peaks in the lines indicate increased positions, while dips show reductions or sales.<\/li>\n<!-- \/wp:list-item --><\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:image {\"id\":11924,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-36.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-36-1024x771.png\" alt=\"\" class=\"wp-image-11924 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-36-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-36-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-36-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-36-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-36-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-36.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\"><strong>The Code<\/strong><\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The code behind these charts fetches data from Polygon\u2019s blockchain and processes the transactions associated with a given wallet ID. It retrieves ERC-1155 and ERC-20 token transaction data, enriches it with market information, and generates visual insights based on trading activity. You can use this code to analyze any Polymarket user\u2019s trades simply by knowing their wallet address.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>Here\u2019s a breakdown of the main functions:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\"><!-- wp:list-item -->\n<li><strong><code>fetch_user_transactions(wallet_address, api_key)<\/code><\/strong>: Fetches all ERC-1155 and ERC-20 transactions for a given wallet from Polygon.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong><code>add_financial_columns()<\/code><\/strong>: Processes transactions to calculate key financial metrics like profit\/loss, total purchase value, and shares held.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong><code>plot_profit_loss_by_trade()<\/code><\/strong>: Generates a bar plot showing profit or loss for each trade.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong><code>plot_shares_over_time()<\/code><\/strong>: Creates a line plot showing cumulative shares over time.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong><code>create_and_save_pie_chart()<\/code><\/strong>: Generates a pie chart that breaks down holdings by market.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong><code>create_and_save_treemap()<\/code><\/strong>: Produces a treemap for holdings based on market and outcome.<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong><code>plot_total_purchase_value()<\/code><\/strong>: Generates a scatter plot showing total purchase value over time.<\/li>\n<!-- \/wp:list-item --><\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:paragraph -->\n<p>This tool offers deep insights into any user\u2019s trading behavior and performance on Polymarket.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport requests\nimport logging\nimport pandas as pd\nimport subprocess\nimport json\nimport time\nfrom dotenv import load_dotenv\nimport plotly.express as px\nimport re\nfrom bs4 import BeautifulSoup\nfrom importlib import reload\nimport numpy as np\nimport argparse\nimport os\nimport subprocess\nimport json\nimport logging\nimport pandas as pd\nfrom dotenv import load_dotenv\n\nlogging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')\nlogger = logging.getLogger(__name__)\n\n\n# Load environment variables\nload_dotenv(\"keys.env\")\n\nprice_cache = {}\n\n# EXCHANGES\nCTF_EXCHANGE = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'\nNEG_RISK_CTF_EXCHANGE = '0x4d97dcd97ec945f40cf65f87097ace5ea0476045'\n\n# SPENDERS FOR EXCHANGES\nNEG_RISK_CTF_EXCHANGE_SPENDER = '0xC5d563A36AE78145C45a50134d48A1215220f80a'\nNEG_RISK_ADAPTER = '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296'\nCTF_EXCHANGE_SPENDER = '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E'\n\nCACHE_EXPIRATION_TIME = 60 * 30  # Cache expiration time in seconds (5 minutes)\nPRICE_CACHE_FILE = '.\/data\/live_price_cache.json'\n\n# Dictionary to cache live prices\nlive_price_cache = {}\n\n\ndef load_price_cache():\n    \"\"\"Load the live price cache from a JSON file.\"\"\"\n    if os.path.exists(PRICE_CACHE_FILE):\n        try:\n            with open(PRICE_CACHE_FILE, 'r') as file:\n                return json.load(file)\n        except json.JSONDecodeError as e:\n            logger.error(f\"Error loading price cache: {e}\")\n            return {}\n    return {}\n\ndef save_price_cache(cache):\n    \"\"\"Save the live price cache to a JSON file.\"\"\"\n    with open(PRICE_CACHE_FILE, 'w') as file:\n        json.dump(cache, file)\n\ndef is_cache_valid(cache_entry, expiration_time=CACHE_EXPIRATION_TIME):\n    \"\"\"\n    Check if the cache entry is still valid based on the current time and expiration time.\n    \"\"\"\n    if not cache_entry:\n        return False\n    cached_time = cache_entry.get('timestamp', 0)\n    return (time.time() - cached_time) &lt; expiration_time\n\n\ndef call_get_live_price(token_id, expiration_time=CACHE_EXPIRATION_TIME):\n    &quot;&quot;&quot;\n    Get live price from cache or update it if expired.\n    &quot;&quot;&quot;\n    logger.info(f&#039;Getting live price for token {token_id}&#039;)\n\n    # Load existing cache\n    price_cache = load_price_cache()\n    cache_key = f&quot;{token_id}&quot;\n\n    # Check if cache is valid\n    if cache_key in price_cache and is_cache_valid(price_cache[cache_key], expiration_time):\n        logger.info(f&#039;Returning cached price for {cache_key}&#039;)\n        return price_cache[cache_key][&#039;price&#039;]\n\n    # If cache is expired or doesn&#039;t exist, fetch live price\n    try:\n        result = subprocess.run(\n            [&#039;python3&#039;, &#039;get_live_price.py&#039;, token_id],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            text=True,\n            check=True\n        )\n        # Parse the live price from the subprocess output\n        output_lines = result.stdout.strip().split(&quot;\\n&quot;)\n        live_price_line = next((line for line in output_lines if &quot;Live price for token&quot; in line), None)\n        if live_price_line:\n            live_price = float(live_price_line.strip().split(&quot;:&quot;)[-1].strip())\n        else:\n            logger.error(&quot;Live price not found in subprocess output.&quot;)\n            return None\n\n        logger.debug(f&quot;Subprocess get_live_price output: {result.stdout}&quot;)\n\n        # Update cache with the new price and timestamp\n        price_cache[cache_key] = {&#039;price&#039;: live_price, &#039;timestamp&#039;: time.time()}\n        save_price_cache(price_cache)\n\n        return live_price\n\n    except subprocess.CalledProcessError as e:\n        logger.error(f&quot;Subprocess get_live_price error: {e.stderr}&quot;)\n        return None\n    except Exception as e:\n        logger.error(f&quot;Error fetching live price: {str(e)}&quot;)\n        return None\n\ndef update_live_price_and_pl(merged_df, contract_token_id, market_slug=None, outcome=None):\n    &quot;&quot;&quot;\n    Calculate the live price and profit\/loss (pl) for each trade in the DataFrame.\n    &quot;&quot;&quot;\n    # Ensure tokenID in merged_df is string\n    merged_df[&#039;tokenID&#039;] = merged_df[&#039;tokenID&#039;].astype(str)\n    contract_token_id = str(contract_token_id)\n\n    # Check for NaN or empty token IDs\n    if not contract_token_id or contract_token_id == &#039;nan&#039;:\n        logger.warning(&quot;Encountered NaN or empty contract_token_id. Skipping.&quot;)\n        return merged_df\n\n    # Add live_price and pl columns if they don&#039;t exist\n    if &#039;live_price&#039; not in merged_df.columns:\n        merged_df[&#039;live_price&#039;] = np.nan\n    if &#039;pl&#039; not in merged_df.columns:\n        merged_df[&#039;pl&#039;] = np.nan\n\n    # Filter rows with the same contract_token_id and outcome\n    merged_df[&#039;outcome&#039;] = merged_df[&#039;outcome&#039;].astype(str)\n    matching_rows = merged_df[(merged_df[&#039;tokenID&#039;] == contract_token_id) &amp;\n                              (merged_df[&#039;outcome&#039;].str.lower() == outcome.lower())]\n\n    if not matching_rows.empty:\n        logger.info(f&#039;Fetching live price for token {contract_token_id}&#039;)\n        live_price = call_get_live_price(contract_token_id)\n        logger.info(f&#039;Live price for token {contract_token_id}: {live_price}&#039;)\n\n        if live_price is not None:\n            try:\n                # Calculate profit\/loss based on the live price\n                price_paid_per_token = matching_rows[&#039;price_paid_per_token&#039;]\n                total_purchase_value = matching_rows[&#039;total_purchase_value&#039;]\n                pl = ((live_price - price_paid_per_token) \/ price_paid_per_token) * total_purchase_value\n\n                # Update the DataFrame with live price and pl\n                merged_df.loc[matching_rows.index, &#039;live_price&#039;] = live_price\n                merged_df.loc[matching_rows.index, &#039;pl&#039;] = pl\n            except Exception as e:\n                logger.error(f&quot;Error calculating live price and profit\/loss: {e}&quot;)\n        else:\n            logger.warning(f&quot;Live price not found for tokenID {contract_token_id}&quot;)\n            merged_df.loc[matching_rows.index, &#039;pl&#039;] = np.nan\n\n    return merged_df\n\n\n\n\ndef find_token_id(market_slug, outcome, market_lookup):\n    &quot;&quot;&quot;Find the token_id based on market_slug and outcome.&quot;&quot;&quot;\n    for market in market_lookup.values():\n        if market[&#039;market_slug&#039;] == market_slug:\n            for token in market[&#039;tokens&#039;]:\n                if token[&#039;outcome&#039;].lower() == outcome.lower():\n                    return token[&#039;token_id&#039;]\n    return None\n\n\ndef fetch_data(url):\n    &quot;&quot;&quot;Fetch data from a given URL and return the JSON response.&quot;&quot;&quot;\n    try:\n        response = requests.get(url, timeout=10)  # You can specify a timeout\n        response.raise_for_status()  # Raise an error for bad responses (4xx, 5xx)\n        return response.json()\n    except requests.exceptions.RequestException as e:\n        logger.error(f&quot;Error fetching data from URL: {url}. Exception: {e}&quot;)\n        return None\n\ndef fetch_all_pages(api_key, token_ids, market_slug_outcome_map, csv_output_dir=&#039;.\/data\/polymarket_trades\/&#039;):\n    page = 1\n    offset = 100\n    retry_attempts = 0\n    all_data = []  # Store all data here\n\n    while True:\n        url = f&quot;https:\/\/api.polygonscan.com\/api?module=account&amp;action=token1155tx&amp;contractaddress={NEG_RISK_CTF_EXCHANGE}&amp;page={page}&amp;offset={offset}&amp;startblock=0&amp;endblock=99999999&amp;sort=desc&amp;apikey={api_key}&quot;\n        logger.info(f&quot;Fetching transaction data for tokens {token_ids}, page: {page}&quot;)\n\n        data = fetch_data(url)\n\n        if data and data[&#039;status&#039;] == &#039;1&#039;:\n            df = pd.DataFrame(data[&#039;result&#039;])\n\n            if df.empty:\n                logger.info(&quot;No more transactions found, ending pagination.&quot;)\n                break  # Exit if there are no more transactions\n\n            all_data.append(df)\n            page += 1  # Go to the next page\n        else:\n            logger.error(f&quot;API response error or no data found for page {page}&quot;)\n            if retry_attempts &lt; 5:\n                retry_attempts += 1\n                time.sleep(retry_attempts)\n            else:\n                break\n\n    if all_data:\n        final_df = pd.concat(all_data, ignore_index=True)  # Combine all pages\n        logger.info(f&quot;Fetched {len(final_df)} transactions across all pages.&quot;)\n        return final_df\n    return None\n\ndef validate_market_lookup(token_ids, market_lookup):\n    valid_token_ids = []\n    invalid_token_ids = []\n\n    for token_id in token_ids:\n        market_slug, outcome = find_market_info(token_id, market_lookup)\n        if market_slug and outcome:\n            valid_token_ids.append(token_id)\n        else:\n            invalid_token_ids.append(token_id)\n\n    logger.info(f&quot;Valid token IDs: {valid_token_ids}&quot;)\n    if invalid_token_ids:\n        logger.warning(f&quot;Invalid or missing market info for token IDs: {invalid_token_ids}&quot;)\n\n    return valid_token_ids\n\n\ndef sanitize_filename(filename):\n    &quot;&quot;&quot;\n    Sanitize the filename by removing or replacing invalid characters.\n    &quot;&quot;&quot;\n    # Replace invalid characters with an underscore\n    return re.sub(r&#039;[\\\\\/*?:&quot;|]', '_', filename)\n\ndef sanitize_directory(directory):\n    \"\"\"\n    Sanitize the directory name by removing or replacing invalid characters.\n    \"\"\"\n    # Replace invalid characters with an underscore\n    return re.sub(r'[\\\\\/*?:\"|]', '_', directory)\n\ndef extract_wallet_ids(leaderboard_url):\n    \"\"\"Scrape the Polymarket leaderboard to extract wallet IDs.\"\"\"\n    logging.info(f\"Fetching leaderboard page: {leaderboard_url}\")\n\n    response = requests.get(leaderboard_url)\n    if response.status_code != 200:\n        logging.error(f\"Failed to load page {leaderboard_url}, status code: {response.status_code}\")\n        return []\n\n    logging.debug(f\"Page loaded successfully, status code: {response.status_code}\")\n\n    soup = BeautifulSoup(response.content, 'html.parser')\n    logging.debug(\"Page content parsed with BeautifulSoup\")\n\n    wallet_ids = []\n\n    # Debug: Check if <a> tags are being found correctly\n    a_tags = soup.find_all('a', href=True)\n    logging.debug(f\"Found {len(a_tags)} <a> tags in the page.\")\n\n    for a_tag in a_tags:\n        href = a_tag['href']\n        logging.debug(f\"Processing href: {href}\")\n        if href.startswith('\/profile\/'):\n            wallet_id = href.split('\/')[-1]\n            wallet_ids.append(wallet_id)\n            logging.info(f\"Extracted wallet ID: {wallet_id}\")\n        else:\n            logging.debug(f\"Skipped href: {href}\")\n\n    return wallet_ids\ndef load_market_lookup(json_path):\n    \"\"\"Load market lookup data from a JSON file.\"\"\"\n    with open(json_path, 'r') as json_file:\n        return json.load(json_file)\n\n\n\n\ndef find_market_info(token_id, market_lookup):\n    \"\"\"Find market_slug and outcome based on tokenID.\"\"\"\n    token_id = str(token_id)  # Ensure token_id is a string\n    if not token_id or token_id == 'nan':\n        logger.warning(\"Token ID is NaN or empty. Skipping lookup.\")\n        return None, None\n\n    logger.debug(f\"Looking up market info for tokenID: {token_id}\")\n\n    for market in market_lookup.values():\n        for token in market['tokens']:\n            if str(token['token_id']) == token_id:\n                logger.debug(\n                    f\"Found market info for tokenID {token_id}: market_slug = {market['market_slug']}, outcome = {token['outcome']}\")\n                return market['market_slug'], token['outcome']\n\n    logger.warning(f\"No market info found for tokenID: {token_id}\")\n    return None, None\n\n\n\n\n\ndef fetch_data(url):\n    \"\"\"Fetch data from a given URL and return the JSON response.\"\"\"\n    response = requests.get(url)\n    return response.json()\n\n\ndef save_to_csv(filename, data, headers, output_dir):\n    \"\"\"Save data to a CSV file in the specified output directory.\"\"\"\n    filepath = os.path.join(output_dir, filename)\n    with open(filepath, 'w', newline='') as file:\n        writer = csv.DictWriter(file, fieldnames=headers)\n        writer.writeheader()\n        for entry in data:\n            writer.writerow(entry)\n    logger.info(f\"Saved data to {filepath}\")\n\n\n\ndef add_timestamps(erc1155_df, erc20_df):\n    \"\"\"\n    Rename timestamp columns and convert them from UNIX to datetime.\n    \"\"\"\n    # Rename the timestamp columns to avoid conflicts during merge\n    erc1155_df.rename(columns={'timeStamp': 'timeStamp_erc1155'}, inplace=True)\n    erc20_df.rename(columns={'timeStamp': 'timeStamp_erc20'}, inplace=True)\n\n    # Convert UNIX timestamps to datetime format\n    erc1155_df['timeStamp_erc1155'] = pd.to_numeric(erc1155_df['timeStamp_erc1155'], errors='coerce')\n    erc20_df['timeStamp_erc20'] = pd.to_numeric(erc20_df['timeStamp_erc20'], errors='coerce')\n\n    erc1155_df['timeStamp_erc1155'] = pd.to_datetime(erc1155_df['timeStamp_erc1155'], unit='s', errors='coerce')\n    erc20_df['timeStamp_erc20'] = pd.to_datetime(erc20_df['timeStamp_erc20'], unit='s', errors='coerce')\n\n    return erc1155_df, erc20_df\n\n\ndef enrich_erc1155_data(erc1155_df, market_lookup):\n    \"\"\"\n    Enrich the ERC-1155 DataFrame with market_slug and outcome based on market lookup.\n    \"\"\"\n\n    def get_market_info(token_id):\n        if pd.isna(token_id) or str(token_id) == 'nan':\n            return 'Unknown', 'Unknown'\n        for market in market_lookup.values():\n            for token in market['tokens']:\n                if str(token['token_id']) == str(token_id):\n                    return market['market_slug'], token['outcome']\n        return 'Unknown', 'Unknown'\n\n    erc1155_df['market_slug'], erc1155_df['outcome'] = zip(\n        *erc1155_df['tokenID'].apply(lambda x: get_market_info(x))\n    )\n\n    return erc1155_df\n\n\n\ndef get_transaction_details_by_hash(transaction_hash, api_key, output_dir='.\/data\/polymarket_trades\/'):\n    \"\"\"\n    Fetch the transaction details by hash from Polygonscan, parse the logs, and save the flattened data as a CSV.\n\n    Args:\n    - transaction_hash (str): The hash of the transaction.\n    - api_key (str): The Polygonscan API key.\n    - output_dir (str): The directory to save the CSV file.\n\n    Returns:\n    - None: Saves the transaction details to a CSV.\n    \"\"\"\n    # Ensure output directory exists\n    os.makedirs(output_dir, exist_ok=True)\n\n    # Construct the API URL for fetching transaction receipt details by hash\n    url = f\"https:\/\/api.polygonscan.com\/api?module=proxy&amp;action=eth_getTransactionReceipt&amp;txhash={transaction_hash}&amp;apikey={api_key}\"\n\n    logger.info(f\"Fetching transaction details for hash: {transaction_hash}\")\n    logger.debug(f\"Request URL: {url}\")\n\n    try:\n        # Fetch transaction details\n        response = requests.get(url)\n        logger.debug(f\"Polygonscan API response status: {response.status_code}\")\n\n        if response.status_code != 200:\n            logger.error(f\"Non-200 status code received: {response.status_code}\")\n            return None\n\n        # Parse the JSON response\n        data = response.json()\n        logger.debug(f\"Response JSON: {data}\")\n\n        # Check if the status is successful\n        if data.get('result') is None:\n            logger.error(f\"Error in API response: {data.get('message', 'Unknown error')}\")\n            return None\n\n        # Extract the logs\n        logs = data['result']['logs']\n        logs_df = pd.json_normalize(logs)\n\n        # Save the logs to a CSV file for easier review\n        csv_filename = os.path.join(output_dir, f\"transaction_logs_{transaction_hash}.csv\")\n        logs_df.to_csv(csv_filename, index=False)\n        logger.info(f\"Parsed logs saved to {csv_filename}\")\n\n        return logs_df\n\n    except Exception as e:\n        logger.error(f\"Exception occurred while fetching transaction details for hash {transaction_hash}: {e}\")\n        return None\ndef add_financial_columns(erc1155_df, erc20_df, wallet_id, market_lookup):\n    \"\"\"\n    Merge the ERC-1155 and ERC-20 dataframes, calculate financial columns,\n    including whether a trade was won or lost, and fetch the latest price for each contract and tokenID.\n    \"\"\"\n    # Merge the two dataframes on the 'hash' column\n    merged_df = pd.merge(erc1155_df, erc20_df, how='outer', on='hash', suffixes=('_erc1155', '_erc20'))\n\n    # Convert wallet ID and columns to lowercase for case-insensitive comparison\n    wallet_id = wallet_id.lower()\n    merged_df['to_erc1155'] = merged_df['to_erc1155'].astype(str).str.lower()\n    merged_df['from_erc1155'] = merged_df['from_erc1155'].astype(str).str.lower()\n\n    # Remove rows where 'tokenID' is NaN or 'nan'\n    merged_df['tokenID'] = merged_df['tokenID'].astype(str)\n    merged_df = merged_df[~merged_df['tokenID'].isnull() &amp; (merged_df['tokenID'] != 'nan')]\n\n\n    # Set transaction type based on wallet address\n    merged_df['transaction_type'] = 'other'\n    merged_df.loc[merged_df['to_erc1155'] == wallet_id, 'transaction_type'] = 'buy'\n    merged_df.loc[merged_df['from_erc1155'] == wallet_id, 'transaction_type'] = 'sell'\n\n    # Calculate the purchase price per token and total dollar value\n    if 'value' in merged_df.columns and 'tokenValue' in merged_df.columns:\n        merged_df['price_paid_per_token'] = merged_df['value'].astype(float) \/ merged_df['tokenValue'].astype(float)\n        merged_df['total_purchase_value'] = merged_df['value'].astype(float) \/ 10**6  # USDC has 6 decimal places\n        merged_df['shares'] = merged_df['total_purchase_value'] \/ merged_df['price_paid_per_token']\n    else:\n        logger.error(\"The necessary columns for calculating purchase price are missing.\")\n        return merged_df\n\n    # Create the 'lost' and 'won' columns\n    merged_df['lost'] = (\n        (merged_df['to_erc1155'] == '0x0000000000000000000000000000000000000000') &amp;\n        (merged_df['transaction_type'] == 'sell') &amp;\n        (merged_df['price_paid_per_token'].isna() | (merged_df['price_paid_per_token'] == 0))\n    ).astype(int)\n\n    merged_df['won'] = (\n        (merged_df['transaction_type'] == 'sell') &amp;\n        (merged_df['price_paid_per_token'] == 1)\n    ).astype(int)\n\n    merged_df.loc[merged_df['lost'] == 1, 'shares'] = 0\n    merged_df.loc[merged_df['lost'] == 1, 'total_purchase_value'] = 0\n\n    # Fetch live prices and calculate profit\/loss (pl)\n    merged_df['tokenID'] = merged_df['tokenID'].astype(str)\n    merged_df = update_latest_prices(merged_df, market_lookup)\n\n    return merged_df\n\ndef plot_profit_loss_by_trade(df, user_info):\n    \"\"\"\n    Create a bar plot to visualize aggregated Profit\/Loss (PL) by trade, with values rounded to two decimal places and formatted as currency.\n\n    Args:\n        df (DataFrame): DataFrame containing trade data, including 'market_slug', 'outcome', and 'pl'.\n        user_info (dict): Dictionary containing user information, such as username, wallet address, and other relevant details.\n    \"\"\"\n    if 'pl' not in df.columns or df['pl'].isnull().all():\n        logger.warning(\"No PL data available for plotting. Skipping plot.\")\n        return\n\n    username = user_info.get(\"username\", \"Unknown User\")\n    wallet_id = user_info.get(\"wallet_address\", \"N\/A\")\n    positions_value = user_info.get(\"positions_value\", \"N\/A\")\n    profit_loss = user_info.get(\"profit_loss\", \"N\/A\")\n    volume_traded = user_info.get(\"volume_traded\", \"N\/A\")\n    markets_traded = user_info.get(\"markets_traded\", \"N\/A\")\n\n    # Combine market_slug and outcome to create a trade identifier\n    df['trade'] = df['market_slug'] + ' (' + df['outcome'] + ')'\n\n    # Aggregate the Profit\/Loss (pl) for each unique trade\n    aggregated_df = df.groupby('trade', as_index=False).agg({'pl': 'sum'})\n\n    # Round PL values to two decimal places\n    aggregated_df['pl'] = aggregated_df['pl'].round(2)\n\n    # Format the PL values with a dollar sign for display\n    aggregated_df['pl_display'] = aggregated_df['pl'].apply(lambda x: f\"${x:,.2f}\")\n\n    # Define a color mapping based on Profit\/Loss sign\n    aggregated_df['color'] = aggregated_df['pl'].apply(lambda x: 'green' if x &gt;= 0 else 'red')\n\n    # Create the plot without using the color axis\n    fig = px.bar(\n        aggregated_df,\n        x='trade',\n        y='pl',\n        title='',\n        labels={'pl': 'Profit\/Loss ($)', 'trade': 'Trade (Market Slug \/ Outcome)'},\n        text='pl_display',\n        color='color',  # Use the color column\n        color_discrete_map={'green': 'green', 'red': 'red'},\n    )\n\n    # Remove the legend if you don't want it\n    fig.update_layout(showlegend=False)\n\n    # Rotate x-axis labels for better readability and set the main title\n    fig.update_layout(\n        title={\n            'text': 'Aggregated Profit\/Loss by Trade',\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 24}\n        },\n        xaxis_tickangle=-45,\n        margin=dict(t=150, l=50, r=50, b=100)\n    )\n\n    # Prepare the subtitle text with user information\n    subtitle_text = (\n        f\"Username: {username} | Positions Value: {positions_value} | \"\n        f\"Profit\/Loss: {profit_loss} | Volume Traded: {volume_traded} | \"\n        f\"Markets Traded: {markets_traded} | Wallet ID: {wallet_id}\"\n    )\n\n    # Add the subtitle as an annotation\n    fig.add_annotation(\n        text=subtitle_text,\n        xref=\"paper\",\n        yref=\"paper\",\n        x=0.5,\n        y=1.02,\n        xanchor='center',\n        yanchor='top',\n        showarrow=False,\n        font=dict(size=14)\n    )\n\n    # Save the plot\n    plot_dir = \".\/plots\/user_trades\"\n    os.makedirs(plot_dir, exist_ok=True)\n    sanitized_username = sanitize_filename(username)\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_aggregated_profit_loss_by_trade.html\")\n    fig.write_html(plot_file)\n\n    logger.info(f\"Aggregated Profit\/Loss by trade plot saved to {plot_file}\")\n\n\n\ndef plot_shares_over_time(df, user_info):\n    \"\"\"\n    Create a line plot to visualize the cumulative number of shares for each token over time.\n    Buy orders add to the position, and sell orders subtract from it.\n\n    Args:\n        df (DataFrame): DataFrame containing trade data, including 'timeStamp_erc1155', 'shares', 'market_slug', 'outcome', and 'transaction_type' ('buy' or 'sell').\n        user_info (dict): Dictionary containing user information, such as username, wallet address, and other relevant details.\n    \"\"\"\n    if 'shares' not in df.columns or df['shares'].isnull().all():\n        logger.warning(\"No 'shares' data available for plotting. Skipping plot.\")\n        return\n\n    username = user_info.get(\"username\", \"Unknown User\")\n\n    # Ensure 'timeStamp_erc1155' is a datetime type, just in case it needs to be converted\n    if df['timeStamp_erc1155'].dtype != 'datetime64[ns]':\n        df['timeStamp_erc1155'] = pd.to_datetime(df['timeStamp_erc1155'], errors='coerce')\n\n    # Drop rows with NaN values in 'timeStamp_erc1155', 'shares', 'market_slug', 'outcome', or 'transaction_type'\n    df = df.dropna(subset=['timeStamp_erc1155', 'shares', 'market_slug', 'outcome', 'transaction_type'])\n\n    # Sort the dataframe by time to ensure the line chart shows the data in chronological order\n    df = df.sort_values(by='timeStamp_erc1155')\n\n    # Combine 'market_slug' and 'outcome' to create a unique label for each token\n    df['token_label'] = df['market_slug'] + \" - \" + df['outcome']\n\n    # Create a column for 'position_change' which adds shares for buys and subtracts shares for sells based on 'transaction_type'\n    df['position_change'] = df.apply(lambda row: row['shares'] if row['transaction_type'] == 'buy' else -row['shares'], axis=1)\n\n    # Group by 'token_label' and calculate the cumulative position\n    df['cumulative_position'] = df.groupby('token_label')['position_change'].cumsum()\n\n    # Forward fill the cumulative position to maintain it between trades\n    df['cumulative_position'] = df.groupby('token_label')['cumulative_position'].ffill()\n\n    # Create the line plot, grouping by 'token_label' for separate lines per token ID\n    fig = px.line(\n        df,\n        x='timeStamp_erc1155',\n        y='cumulative_position',\n        color='token_label',  # This ensures each token ID (market_slug + outcome) gets its own line\n        title=f'Cumulative Shares Over Time for {username}',\n        labels={'timeStamp_erc1155': 'Trade Time', 'cumulative_position': 'Cumulative Position', 'token_label': 'Token (Market Slug - Outcome)'},\n        line_shape='linear'\n    )\n\n    # Update layout for better aesthetics\n    fig.update_layout(\n        title={\n            'text': f\"Cumulative Number of Shares Over Time for {username}\",\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 20}\n        },\n        margin=dict(t=60),\n        xaxis_title=\"Trade Time\",\n        yaxis_title=\"Cumulative Number of Shares\",\n        legend_title=\"Token (Market Slug - Outcome)\"\n    )\n\n    # Save the plot\n    plot_dir = \".\/plots\/user_trades\"\n    os.makedirs(plot_dir, exist_ok=True)\n    sanitized_username = sanitize_filename(username)\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_shares_over_time.html\")\n    fig.write_html(plot_file)\n\n    logger.info(f\"Cumulative shares over time plot saved to {plot_file}\")\n\n\ndef plot_user_trades(df, user_info):\n    \"\"\"Plot user trades and save plots, adjusting for trades that were lost.\"\"\"\n    username = user_info[\"username\"]\n    wallet_id = user_info[\"wallet_address\"]\n\n    # Sanitize only the filename, not the directory\n    sanitized_username = sanitize_filename(username)\n\n    info_text = (\n        f\"Username: {username} | Positions Value: {user_info['positions_value']} | \"\n        f\"Profit\/Loss: {user_info['profit_loss']} | Volume Traded: {user_info['volume_traded']} | \"\n        f\"Markets Traded: {user_info['markets_traded']} | Wallet ID: {wallet_id}\"\n    )\n\n    # Ensure the directory exists\n    os.makedirs(\".\/plots\/user_trades\", exist_ok=True)\n    plot_dir = \".\/plots\/user_trades\"\n\n    # Flag loss trades where to_erc1155 is zero address, transaction_type is sell, and price_paid_per_token is NaN\n    df['is_loss'] = df.apply(\n        lambda row: (row['to_erc1155'] == '0x0000000000000000000000000000000000000000')\n                    and (row['transaction_type'] == 'sell')\n                    and pd.isna(row['price_paid_per_token']), axis=1)\n\n    # Set shares and total purchase value to zero for loss trades\n    df.loc[df['is_loss'], 'shares'] = 0\n    df.loc[df['is_loss'], 'total_purchase_value'] = 0\n\n    ### Modify for Total Purchase Value by Market (Current holdings)\n    df['total_purchase_value_adjusted'] = df.apply(\n        lambda row: row['total_purchase_value'] if row['transaction_type'] == 'buy' else -row['total_purchase_value'],\n        axis=1\n    )\n\n    grouped_df_value = df.groupby(['market_slug']).agg({\n        'total_purchase_value_adjusted': 'sum',\n        'shares': 'sum',\n    }).reset_index()\n\n    # Calculate the weighted average price_paid_per_token\n    grouped_df_value['weighted_price_paid_per_token'] = (\n        grouped_df_value['total_purchase_value_adjusted'] \/ grouped_df_value['shares']\n    )\n\n    # Sort by total_purchase_value in descending order (ignoring outcome)\n    grouped_df_value = grouped_df_value.sort_values(by='total_purchase_value_adjusted', ascending=False)\n\n    # Format the label for the bars (removing outcome)\n    grouped_df_value['bar_label'] = (\n        \"Avg Price: $\" + grouped_df_value['weighted_price_paid_per_token'].round(2).astype(str)\n    )\n\n    fig = px.bar(\n        grouped_df_value,\n        x='market_slug',\n        y='total_purchase_value_adjusted',\n        barmode='group',\n        title=f\"Current Total Purchase Value by Market for {username}\",\n        labels={'total_purchase_value_adjusted': 'Current Total Purchase Value', 'market_slug': 'Market'},\n        text=grouped_df_value['bar_label'],\n        hover_data={'weighted_price_paid_per_token': ':.2f'},\n    )\n\n    fig.update_layout(\n        title={\n            'text': f\"Current Total Purchase Value by Market for {username}\",\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 20}\n        },\n        margin=dict(t=60),\n        showlegend=False  # Remove the legend as you requested\n    )\n\n    fig.add_annotation(\n        text=info_text,\n        xref=\"paper\", yref=\"paper\", showarrow=False, x=0.5, y=1.05, font=dict(size=12)\n    )\n\n    # Save the bar plot as an HTML file\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_current_market_purchase_value.html\")\n    fig.write_html(plot_file)\n    logger.info(f\"Current market purchase value plot saved to {plot_file}\")\n\n    ### Modify for Trade Quantity by Market (Current holdings)\n    df['shares_adjusted'] = df.apply(\n        lambda row: row['shares'] if row['transaction_type'] == 'buy' else -row['shares'], axis=1)\n\n    grouped_df_quantity = df.groupby(['market_slug']).agg({\n        'shares_adjusted': 'sum',\n        'total_purchase_value': 'sum',\n    }).reset_index()\n\n    # Calculate the weighted average price_paid_per_token\n    grouped_df_quantity['weighted_price_paid_per_token'] = (\n        grouped_df_quantity['total_purchase_value'] \/ grouped_df_quantity['shares_adjusted']\n    )\n\n    grouped_df_quantity = grouped_df_quantity.sort_values(by='shares_adjusted', ascending=False)\n\n    grouped_df_quantity['bar_label'] = (\n        \"Quantity: \" + grouped_df_quantity['shares_adjusted'].round().astype(int).astype(str) + \"<br>\" +\n        \"Avg Price: $\" + grouped_df_quantity['weighted_price_paid_per_token'].round(2).astype(str)\n    )\n\n    fig = px.bar(\n        grouped_df_quantity,\n        x='market_slug',\n        y='shares_adjusted',\n        barmode='group',\n        title=f\"Current Trade Quantity by Market for {username}\",\n        labels={'shares_adjusted': 'Current Trade Quantity', 'market_slug': 'Market'},\n        text=grouped_df_quantity['bar_label'],\n    )\n\n    fig.update_layout(\n        title={\n            'text': f\"Current Trade Quantity by Market for {username}\",\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 20}\n        },\n        margin=dict(t=60),\n        showlegend=False  # Remove the legend as you requested\n    )\n\n    fig.add_annotation(\n        text=info_text,\n        xref=\"paper\", yref=\"paper\", showarrow=False, x=0.5, y=1.05, font=dict(size=12)\n    )\n\n    # Save the trade quantity plot as an HTML file\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_current_market_trade_quantity.html\")\n    fig.write_html(plot_file)\n    logger.info(f\"Current market trade quantity plot saved to {plot_file}\")\n\n    ### Modify for Total Purchase Value Timeline\n    df['total_purchase_value_timeline_adjusted'] = df.apply(\n        lambda row: row['total_purchase_value'] if row['transaction_type'] == 'buy' else -row['total_purchase_value'],\n        axis=1\n    )\n\n    # Combine 'market_slug' and 'outcome' into a unique label\n    df['market_outcome_label'] = df['market_slug'] + ' (' + df['outcome'] + ')'\n\n    # Create the scatter plot, now coloring by 'market_outcome_label'\n    fig = px.scatter(\n        df,\n        x='timeStamp_erc1155',\n        y='total_purchase_value_timeline_adjusted',\n        color='market_outcome_label',  # Use the combined label for market and outcome\n        title=f\"Total Purchase Value Timeline for {username}\",\n        labels={\n            'total_purchase_value_timeline_adjusted': 'Total Purchase Value',\n            'timeStamp_erc1155': 'Transaction Time',\n            'market_outcome_label': 'Market\/Outcome'\n        },\n        hover_data=['market_slug', 'price_paid_per_token', 'outcome', 'hash'],\n    )\n\n    fig.update_layout(\n        title={\n            'text': f\"Total Purchase Value Timeline for {username}\",\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 20}\n        },\n        margin=dict(t=60)\n    )\n\n    fig.add_annotation(\n        text=info_text,\n        xref=\"paper\", yref=\"paper\", showarrow=False, x=0.5, y=1.05, font=dict(size=12)\n    )\n\n    # Save the updated plot\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_total_purchase_value_timeline_adjusted.html\")\n    fig.write_html(plot_file)\n    logger.info(f\"Total purchase value timeline plot saved to {plot_file}\")\n\ndef plot_total_purchase_value(df, user_info):\n    \"\"\"Create and save a scatter plot for total purchase value, accounting for buy and sell transactions.\"\"\"\n    # Ensure the directory exists\n    os.makedirs(\".\/plots\/user_trades\", exist_ok=True)\n    plot_dir = \".\/plots\/user_trades\"\n\n    username = user_info[\"username\"]\n    wallet_id = user_info[\"wallet_address\"]\n\n    # Sanitize only the filename, not the directory\n    sanitized_username = sanitize_filename(username)\n\n    info_text = (\n        f\"Username: {username} | Positions Value: {user_info['positions_value']} | \"\n        f\"Profit\/Loss: {user_info['profit_loss']} | Volume Traded: {user_info['volume_traded']} | \"\n        f\"Markets Traded: {user_info['markets_traded']} | Wallet ID: {wallet_id}\"\n    )\n\n    # Flag loss trades where to_erc1155 is zero address, transaction_type is sell, and price_paid_per_token is NaN\n    df['is_loss'] = df.apply(\n        lambda row: (row['to_erc1155'] == '0x0000000000000000000000000000000000000000')\n                    and (row['transaction_type'] == 'sell')\n                    and pd.isna(row['price_paid_per_token']), axis=1)\n\n    # Set shares and total purchase value to zero for loss trades\n    df.loc[df['is_loss'], 'shares'] = 0\n    df.loc[df['is_loss'], 'total_purchase_value'] = 0\n\n    # Adjust the total purchase value based on the transaction type\n    df['total_purchase_value_adjusted'] = df.apply(\n        lambda row: row['total_purchase_value'] if row['transaction_type'] == 'buy' else -row['total_purchase_value'],\n        axis=1\n    )\n\n    # Create the scatter plot for total purchase value over time\n    fig = px.scatter(\n        df,\n        x='timeStamp_erc1155',  # Assuming this is the correct timestamp field\n        y='total_purchase_value_adjusted',  # Adjusted values for buys and sells\n        color='market_slug',  # Use market_slug with outcome as the color\n        title=f\"Current Purchase Value Timeline for {username}\",  # Update title to reflect \"current\"\n        labels={'total_purchase_value_adjusted': 'Adjusted Purchase Value ($)', 'timeStamp_erc1155': 'Transaction Time'},\n        hover_data=['market_slug', 'price_paid_per_token', 'outcome', 'hash'],\n    )\n\n    # Adjust title positioning and font size\n    fig.update_layout(\n        title={\n            'text': f\"Current Purchase Value Timeline for {username}\",  # Update to \"Current\"\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 20}\n        },\n        margin=dict(t=60)\n    )\n\n    fig.add_annotation(\n        text=info_text,\n        xref=\"paper\", yref=\"paper\", showarrow=False, x=0.5, y=1.05, font=dict(size=12)\n    )\n\n    # Save the scatter plot as an HTML file\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_current_purchase_value_timeline.html\")\n    fig.write_html(plot_file)\n    logger.info(f\"Current purchase value timeline plot saved to {plot_file}\")\n\ndef create_and_save_pie_chart(df, user_info):\n    \"\"\"Create and save a pie chart for user's current holdings.\"\"\"\n    # Ensure the directory exists\n    os.makedirs(\".\/plots\/user_trades\", exist_ok=True)\n    plot_dir = \".\/plots\/user_trades\"\n    username = user_info[\"username\"]\n    wallet_id = user_info[\"wallet_address\"]\n\n    sanitized_username = sanitize_filename(username)\n\n    info_text = (\n        f\"Username: {username} | Positions Value: {user_info['positions_value']} | \"\n        f\"Profit\/Loss: {user_info['profit_loss']} | Volume Traded: {user_info['volume_traded']} | \"\n        f\"Markets Traded: {user_info['markets_traded']} | Wallet ID: {wallet_id}\"\n    )\n\n    # Flag loss trades where to_erc1155 is zero address, transaction_type is sell, and price_paid_per_token is NaN\n    df['is_loss'] = df.apply(\n        lambda row: (row['to_erc1155'] == '0x0000000000000000000000000000000000000000')\n                    and (row['transaction_type'] == 'sell')\n                    and pd.isna(row['price_paid_per_token']), axis=1)\n\n    # Set shares and total purchase value to zero for loss trades\n    df.loc[df['is_loss'], 'shares'] = 0\n\n    df['shares_adjusted'] = df.apply(\n        lambda row: row['shares'] if row['transaction_type'] == 'buy' else -row['shares'], axis=1)\n\n    holdings = df.groupby('market_slug').agg({'shares_adjusted': 'sum'}).reset_index()\n\n    holdings = holdings.sort_values('shares_adjusted', ascending=False)\n    threshold = 0.02\n    large_slices = holdings[holdings['shares_adjusted'] &gt; holdings['shares_adjusted'].sum() * threshold]\n    small_slices = holdings[holdings['shares_adjusted']  0:\n                all_data.extend(data['result'])\n                page += 1\n            else:\n                break  # Stop if no more data is returned\n\n        return pd.DataFrame(all_data)\n\n    # Fetch ERC-20 transactions with pagination\n    erc20_url = (f\"https:\/\/api.polygonscan.com\/api\"\n                 f\"?module=account\"\n                 f\"&amp;action=tokentx\"\n                 f\"&amp;address={wallet_address}\"\n                 f\"&amp;startblock=0\"\n                 f\"&amp;endblock=99999999\"\n                 f\"&amp;sort=desc\"\n                 f\"&amp;apikey={api_key}\")\n\n    erc20_df = fetch_paginated_data(erc20_url)\n\n    # Fetch ERC-1155 transactions with pagination\n    erc1155_url = (f\"https:\/\/api.polygonscan.com\/api\"\n                   f\"?module=account\"\n                   f\"&amp;action=token1155tx\"\n                   f\"&amp;address={wallet_address}\"\n                   f\"&amp;startblock=0\"\n                   f\"&amp;endblock=99999999\"\n                   f\"&amp;sort=desc\"\n                   f\"&amp;apikey={api_key}\")\n\n    erc1155_df = fetch_paginated_data(erc1155_url)\n\n    if not erc20_df.empty and not erc1155_df.empty:\n        return erc20_df, erc1155_df\n    else:\n        return None, None\n\n\ndef fetch_wallet_addresses(skip_leaderboard, top_volume, top_profit):\n    \"\"\"\n    Fetch wallet addresses based on leaderboard data or manual input.\n\n    Args:\n        skip_leaderboard (bool): Whether to skip leaderboard fetching.\n        top_volume (bool): Fetch top volume users.\n        top_profit (bool): Fetch top profit users.\n\n    Returns:\n        list: A list of wallet addresses to process.\n    \"\"\"\n    # Manually specified wallet addresses\n    manual_wallet_ids = [\n        '0x76527252D7FEd00dC4D08d794aFa1cCC36069C2a',\n        # Add more wallet IDs as needed\n    ]\n\n    if not skip_leaderboard:\n        leaderboard_wallet_ids = call_scrape_wallet_ids(top_volume=top_volume, top_profit=top_profit)\n        wallet_addresses = list(set(manual_wallet_ids + leaderboard_wallet_ids))  # Remove duplicates\n    else:\n        wallet_addresses = manual_wallet_ids\n\n    return wallet_addresses\n\ndef main(wallet_addresses=None, skip_leaderboard=False, top_volume=False, top_profit=False, plot=True, latest_price_mode=False):\n\n    \"\"\"\n    Main function to process wallet data and generate plots.\n\n    Args:\n        wallet_addresses (list): A list of wallet addresses to process (if provided).\n        skip_leaderboard (bool): Whether to skip fetching leaderboard data.\n        top_volume (bool): Whether to fetch top volume users.\n        top_profit (bool): Whether to fetch top profit users.\n        plot (bool): Whether to generate plots for the user data.\n        latest_price_mode (bool): If True, only retrieve the latest prices, no plotting.\n    \"\"\"\n    # Load environment variables\n    load_dotenv(\"keys.env\")\n    api_key = os.getenv('POLYGONSCAN_API_KEY')\n\n    if not wallet_addresses:\n        # Fetch wallet addresses if not provided\n        wallet_addresses = fetch_wallet_addresses(skip_leaderboard, top_volume, top_profit)\n\n    # Process wallet data and optionally generate plots\n    process_and_plot_user_data(wallet_addresses, api_key, plot=plot, latest_price_mode=latest_price_mode)\n\n\nif __name__ == \"__main__\":\n    # Use argparse to accept command-line arguments\n    parser = argparse.ArgumentParser(description='Process wallet data for specific wallet addresses.')\n\n    parser.add_argument(\n        '--wallets',\n        nargs='+',  # This will accept multiple wallet IDs\n        help='List of wallet addresses to process.'\n    )\n    parser.add_argument('--skip-leaderboard', action='store_true', help='Skip leaderboard fetching.')\n    parser.add_argument('--top-volume', action='store_true', help='Fetch top volume users.')\n    parser.add_argument('--top-profit', action='store_true', help='Fetch top profit users.')\n    parser.add_argument('--no-plot', action='store_true', help='Disable plot generation.')\n    parser.add_argument('--latest-price-mode', action='store_true',\n                        help='Only retrieve the latest prices, no plotting.')\n\n    args = parser.parse_args()\n\n    # Call the main function with the parsed arguments\n    main(\n        wallet_addresses=args.wallets,\n        skip_leaderboard=args.skip_leaderboard,\n        top_volume=args.top_volume,\n        top_profit=args.top_profit,\n        plot=not args.no_plot,\n        latest_price_mode=args.latest_price_mode\n    )<\/pre>\n<!-- \/wp:enlighter\/codeblock --><\/div><\/li>\n<li><div class=\"wp-block-latest-posts__featured-image\"><img decoding=\"async\" width=\"150\" height=\"54\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/Screenshot-2024-09-24-121604-150x54.png\" class=\"attachment-thumbnail size-thumbnail wp-post-image lazyload\" alt=\"\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 150px; --smush-placeholder-aspect-ratio: 150\/54;\" \/><\/div><a class=\"wp-block-latest-posts__post-title\" href=\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/09\/24\/arbitrage-in-polymarket-com\/\">Arbitrage in Polymarket.com<\/a><div class=\"wp-block-latest-posts__post-author\">by JeremyWhittaker<\/div><time datetime=\"2024-09-24T12:16:46-07:00\" class=\"wp-block-latest-posts__post-date\">September 24, 2024<\/time><div class=\"wp-block-latest-posts__post-full-content\"><!-- wp:paragraph -->\n<p>About a month ago, I noticed some arbitrage opportunities on Polymarket.com. However, the liquidity on the platform isn&#8217;t significant enough for me to pursue this further. I&#8217;m planning to return to stock trading. That said, I still have around $60,000 actively invested in various markets on Polymarket. Most of these trades will expire after the election, so I\u2019ll close them out then. Below, I\u2019ll walk you through these trades and offer some free code for anyone interested in exploring this market.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Arb 1: Harris for President and buy all GOP Electoral College Margins<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The strategy here is straightforward: We\u2019re betting that Kamala Harris will win the presidency, while simultaneously buying all the GOP Electoral College margins. Essentially, these two positions are opposites. If the total of both bets is less than 1, that difference represents the percentage of guaranteed profit\u2014our arbitrage. As of today, this trade is yielding a 3.5% arbitrage with 41 days remaining until the election. That translates to a ~41% annualized return.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>Below, you\u2019ll find a plot that shows how I\u2019ve structured my trades. You can see I\u2019ve bought nearly equal amounts of shares in all possible outcomes.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"lightbox\":{\"enabled\":false},\"id\":11877,\"width\":\"685px\",\"height\":\"auto\",\"sizeSlug\":\"large\",\"linkDestination\":\"custom\",\"align\":\"center\"} -->\n<figure class=\"wp-block-image aligncenter size-large is-resized\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-25.png\"><img decoding=\"async\" width=\"1024\" height=\"398\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1024x398.png\" alt=\"\" class=\"wp-image-11877 lazyload\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/398;width:685px;height:auto\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1024x398.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-300x117.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-768x299.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1536x598.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-500x195.png 500w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image.png 1573w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:paragraph -->\n<p>Here are my trades on<a href=\"https:\/\/polymarket.com\"> Polymarket.com<\/a>. You can see I&#8217;ve essentially bought the same number of shares of each of possible outcomes. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11882,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"928\" height=\"54\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-2.png\" alt=\"\" class=\"wp-image-11882 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-2.png 928w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-2-300x17.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-2-768x45.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-2-500x29.png 500w\" data-sizes=\"(max-width: 928px) 100vw, 928px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 928px; --smush-placeholder-aspect-ratio: 928\/54;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11883,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"930\" height=\"461\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-3.png\" alt=\"\" class=\"wp-image-11883 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-3.png 930w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-3-300x149.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-3-768x381.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-3-500x248.png 500w\" data-sizes=\"(max-width: 930px) 100vw, 930px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 930px; --smush-placeholder-aspect-ratio: 930\/461;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:paragraph -->\n<p>Here\u2019s a summary of my trades placed live on Polymarket. The average cost of these positions is 0.983, meaning my expected return is 1 &#8211; 0.983, or 1.7%. My last trade had a cost basis of 0.979, yielding a 2.1% return.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11880,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1.png\"><img decoding=\"async\" width=\"1024\" height=\"314\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1-1024x314.png\" alt=\"\" class=\"wp-image-11880 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1-1024x314.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1-300x92.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1-768x236.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1-1536x471.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1-500x153.png 500w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-1.png 1692w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/314;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Arb 2: Trump for President hedged with DEMS to win the popular vote and presidency<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This strategy is showing a 2.55% arbitrage. In this scenario, we\u2019re betting on Trump to win, while hedging by betting that the Democrats will win both the popular vote and the presidency. While this is not a perfect hedge (since it\u2019s possible for the Democrats to win the presidency without the popular vote), based on my modeling, this scenario is highly unlikely. Therefore, I consider this hedge to be sound.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"lightbox\":{\"enabled\":false},\"id\":11884,\"sizeSlug\":\"large\",\"linkDestination\":\"custom\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-26.png\"><img decoding=\"async\" width=\"1024\" height=\"376\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-4-1024x376.png\" alt=\"\" class=\"wp-image-11884 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-4-1024x376.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-4-300x110.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-4-768x282.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-4-1536x564.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-4-500x184.png 500w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-4.png 1612w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/376;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:paragraph -->\n<p>Below are my actual trades, demonstrating that I have an equal number of shares for each side of the bet.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11886,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"927\" height=\"116\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-5.png\" alt=\"\" class=\"wp-image-11886 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-5.png 927w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-5-300x38.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-5-768x96.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-5-500x63.png 500w\" data-sizes=\"(max-width: 927px) 100vw, 927px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 927px; --smush-placeholder-aspect-ratio: 927\/116;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Arb 3: Buy all the no outcomes for DEM and GOP popular vote<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>For this trade, I\u2019ve bought the &#8220;No&#8221; outcome on every possible bet for the popular vote. This currently presents a 6.65% arbitrage opportunity.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>Here are the actual trades. As you can see, all but one of these bets will win on election day. The total profit from the winning trades, minus the loss from the single losing trade, results in the overall return.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"lightbox\":{\"enabled\":false},\"id\":11887,\"sizeSlug\":\"large\",\"linkDestination\":\"custom\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-27.png\"><img decoding=\"async\" width=\"1024\" height=\"190\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-6-1024x190.png\" alt=\"\" class=\"wp-image-11887 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-6-1024x190.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-6-300x56.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-6-768x142.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-6-1536x285.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-6-500x93.png 500w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-6.png 1618w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/190;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:paragraph -->\n<p>Here are my actual trades.  All of these except 1 will win on election day. So the winning side of all these trades less 1 needs to be greater than the amount lost on the losing trade. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11889,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"937\" height=\"922\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-7.png\" alt=\"\" class=\"wp-image-11889 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-7.png 937w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-7-300x295.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-7-768x756.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-7-305x300.png 305w\" data-sizes=\"(max-width: 937px) 100vw, 937px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 937px; --smush-placeholder-aspect-ratio: 937\/922;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Last arb:<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>I wrote extensively about betting against Nate Silver and 538 predictions in an older blog post <a href=\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/08\/30\/exploiting-arbitrage-betting-against-nate-silver-for-a-54-yield\/\">here<\/a>. This goes in-depth on how I will trade these two markets. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>I\u2019ve written in-depth about my strategy of betting against Nate Silver and the predictions from FiveThirtyEight in a previous blog post, which you can find <a href=\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/08\/30\/exploiting-arbitrage-betting-against-nate-silver-for-a-54-yield\/\">here<\/a>. That post details how I plan to trade these two markets.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11894,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"939\" height=\"69\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-12.png\" alt=\"\" class=\"wp-image-11894 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-12.png 939w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-12-300x22.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-12-768x56.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-12-500x37.png 500w\" data-sizes=\"(max-width: 939px) 100vw, 939px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 939px; --smush-placeholder-aspect-ratio: 939\/69;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11895,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"933\" height=\"59\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-13.png\" alt=\"\" class=\"wp-image-11895 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-13.png 933w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-13-300x19.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-13-768x49.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-13-500x32.png 500w\" data-sizes=\"(max-width: 933px) 100vw, 933px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 933px; --smush-placeholder-aspect-ratio: 933\/59;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Read the Rules Carefully<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>One important tip: Always read the rules of each trade carefully. Some positions may seem like arbitrage opportunities, but they can carry hidden risks. For example, if a candidate were to be assassinated, you could lose all your money, despite thinking you had a safe arbitrage position.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">Spread Matters<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>One of the biggest challenges I\u2019ve encountered is market impact. When I place a trade, I often move the entire market in my direction due to the low liquidity. This creates discrepancies between the bid, ask, mid, live, and actual trade prices. Below are examples of this from the trades I shared earlier.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11890,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"970\" height=\"333\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-8.png\" alt=\"\" class=\"wp-image-11890 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-8.png 970w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-8-300x103.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-8-768x264.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-8-500x172.png 500w\" data-sizes=\"(max-width: 970px) 100vw, 970px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 970px; --smush-placeholder-aspect-ratio: 970\/333;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11891,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"967\" height=\"347\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-9.png\" alt=\"\" class=\"wp-image-11891 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-9.png 967w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-9-300x108.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-9-768x276.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-9-500x179.png 500w\" data-sizes=\"(max-width: 967px) 100vw, 967px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 967px; --smush-placeholder-aspect-ratio: 967\/347;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11892,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"919\" height=\"343\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-10.png\" alt=\"\" class=\"wp-image-11892 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-10.png 919w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-10-300x112.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-10-768x287.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-10-500x187.png 500w\" data-sizes=\"(max-width: 919px) 100vw, 919px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 919px; --smush-placeholder-aspect-ratio: 919\/343;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11893,\"sizeSlug\":\"full\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"970\" height=\"721\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-11.png\" alt=\"\" class=\"wp-image-11893 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-11.png 970w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-11-300x223.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-11-768x571.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-11-404x300.png 404w\" data-sizes=\"(max-width: 970px) 100vw, 970px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 970px; --smush-placeholder-aspect-ratio: 970\/721;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">The Code:<\/h2>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>I spent a tremendous amount of time writing code to break down Polymarkets data. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">strategies.py <\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This file contains all of the hedge opportunities that I&#8217;m monitoring. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">trades = [\n\n    {\n        \"trade_name\": \"Harris for president and all GOP electoral college margins\",\n        \"subtitle\": \"This trade assumes Harris winning is the opposite of the GOP winning every category \"\n                    \"in the electoral college. The risk is Harris could be replaced or die causing one \"\n                    \"side of this trade to not work.\",\n        \"side_a_trades\": [\n            (\"2024-presidential-election-gop-wins-by-1-4\", \"Yes\"),\n            (\"2024-presidential-election-gop-wins-by-5-14\", \"Yes\"),\n            (\"2024-presidential-election-gop-wins-by-15-34\", \"Yes\"),\n            (\"2024-presidential-election-gop-wins-by-35-64\", \"Yes\"),\n            (\"2024-presidential-election-gop-wins-by-65-104\", \"Yes\"),\n            (\"2024-presidential-election-gop-wins-by-105-154\", \"Yes\"),\n            (\"2024-presidential-election-gop-wins-by-155-214\", \"Yes\"),\n            (\"2024-presidential-election-gop-wins-by-215\", \"Yes\"),\n        ],\n        \"side_b_trades\": [\n            (\"will-kamala-harris-win-the-2024-us-presidential-election\", \"Yes\"),\n        ],\n        \"method\": \"balanced\"\n    },\n\n\n\n    {\n        \"trade_name\": \"DEM and REP electoral college all no\",\n        \"subtitle\": \"bet no on all DEM and REP electoral college positions\",\n        \"positions\": [\n            (\"2024-presidential-election-gop-wins-by-215\", \"No\"),\n            (\"2024-presidential-election-gop-wins-by-155-214\", \"No\"),\n            (\"2024-presidential-election-gop-wins-by-65-104\", \"No\"),\n            (\"2024-presidential-election-gop-wins-by-35-64\", \"No\"),\n            (\"2024-presidential-election-gop-wins-by-15-34\", \"No\"),\n            (\"2024-presidential-election-gop-wins-by-1-4\", \"No\"),\n            (\"2024-presidential-election-gop-wins-by-5-14\", \"No\"),\n            (\"2024-presidential-election-gop-wins-by-105-154\", \"No\"),\n            (\"2024-presidential-election-democrats-win-by-0-4\", \"No\"),\n            (\"2024-presidential-election-democrats-win-by-5-14\", \"No\"),\n            (\"2024-presidential-election-democrats-win-by-15-34\", \"No\"),\n            (\"2024-presidential-election-democrats-win-by-35-64\", \"No\"),\n            (\"2024-presidential-election-democrats-win-by-65-104\", \"No\"),\n            (\"2024-presidential-election-democrats-win-by-105-154\", \"No\"),\n            (\"2024-presidential-election-democrats-win-by-155-214\", \"No\"),\n            (\"2024-presidential-election-democrats-win-by-215\", \"No\"),\n        ],\n        \"method\": \"all_no\"\n    },\n\n    {\n        \"trade_name\": \"DEM and GOP popular vote all no\",\n        \"subtitle\": \"bet no on all DEM and GOP popular vote positions\",\n        \"positions\": [\n            (\"gop-wins-popular-vote-by-more-than-7\", \"No\"),\n            (\"gop-wins-popular-vote-by-6-7\", \"No\"),\n            (\"gop-wins-popular-vote-by-5-6\", \"No\"),\n            (\"gop-wins-popular-vote-by-4-5\", \"No\"),\n            (\"gop-wins-popular-vote-by-3-4\", \"No\"),\n            (\"gop-wins-popular-vote-by-2-3\", \"No\"),\n            (\"gop-wins-popular-vote-by-1-2\", \"No\"),\n            (\"gop-wins-popular-vote-by-0-1\", \"No\"),\n\n            (\"democrats-win-popular-vote-by-over-7\", \"No\"),\n            (\"democrats-win-popular-vote-by-6-7\", \"No\"),\n            (\"democrats-win-popular-vote-by-5-6\", \"No\"),\n            (\"democrats-win-popular-vote-by-4-5\", \"No\"),\n            (\"democrats-win-popular-vote-by-3-4\", \"No\"),\n            (\"democrats-win-popular-vote-by-2-3\", \"No\"),\n            (\"democrats-win-popular-vote-by-1-2\", \"No\"),\n            (\"democrats-win-popular-vote-by-0-1\", \"No\"),\n\n        ],\n        \"method\": \"all_no\"\n    },\n\n    {\n        \"trade_name\": \"\"\n                      \"Trump for president and DEMS win presidency and popular\",\n        \"subtitle\": \"If DEMS win presidency they will win popular vote so this is a direct hedge \"\n                    \"on Trump getting elected \",\n        \"side_a_trades\": [\n            (\"will-a-democrat-win-the-popular-vote-and-the-presidency\", \"Yes\"),\n        ],\n        \"side_b_trades\": [\n            (\"will-donald-trump-win-the-2024-us-presidential-election\", \"Yes\"),\n        ],\n        \"method\": \"balanced\"\n    },\n\n    {\n        \"trade_name\": \"\"\n                      \"DEM win presidency hedged on Trump\",\n        \"subtitle\": \"REP win presidency hedged on Kamala winning\",\n        \"side_a_trades\": [\n            (\"which-party-will-win-the-2024-united-states-presidential-election\", \"Democratic\"),\n        ],\n        \"side_b_trades\": [\n            (\"will-donald-trump-win-the-2024-us-presidential-election\", \"Yes\"),\n        ],\n        \"method\": \"balanced\"\n    },\n\n    {\n        \"trade_name\": \"\"\n                      \"REP win presidency hedged on Kamala\",\n        \"subtitle\": \"REP win presidency hedged on Kamala winning\",\n        \"side_a_trades\": [\n            (\"which-party-will-win-the-2024-united-states-presidential-election\", \"Republican\"),\n        ],\n        \"side_b_trades\": [\n            (\"will-kamala-harris-win-the-2024-us-presidential-election\", \"Yes\"),\n        ],\n        \"method\": \"balanced\"\n    },\n\n    {\n        \"trade_name\": \"\"\n                      \"Trump popular vote hedged with popular vote margins\",\n        \"subtitle\": \"Trump wins popular vote and then buy all the margins for DEMS on the popular vote\",\n        \"side_a_trades\": [\n            (\"will-donald-trump-win-the-popular-vote-in-the-2024-presidential-election\", \"Yes\"),\n        ],\n        \"side_b_trades\": [\n            (\"democrats-win-popular-vote-by-0-1\", \"Yes\"),\n            (\"democrats-win-popular-vote-by-1-2\", \"Yes\"),\n            (\"democrats-win-popular-vote-by-2-3\", \"Yes\"),\n            (\"democrats-win-popular-vote-by-3-4\", \"Yes\"),\n            (\"democrats-win-popular-vote-by-4-5\", \"Yes\"),\n            (\"democrats-win-popular-vote-by-5-6\", \"Yes\"),\n            (\"democrats-win-popular-vote-by-6-7\", \"Yes\"),\n            (\"democrats-win-popular-vote-by-over-7\", \"Yes\"),\n\n        ],\n        \"method\": \"balanced\"\n    },\n\n    {\n        \"trade_name\": \"\"\n                      \"Kamala popular vote hedged with popular vote margins\",\n        \"subtitle\": \"Kamala wins popular vote and then buy all the margins for REP on the popular vote\",\n        \"side_a_trades\": [\n            (\"will-kamala-harris-win-the-popular-vote-in-the-2024-presidential-election\", \"Yes\"),\n        ],\n        \"side_b_trades\": [\n            (\"gop-wins-popular-vote-by-0-1\", \"Yes\"),\n            (\"gop-wins-popular-vote-by-1-2\", \"Yes\"),\n            (\"gop-wins-popular-vote-by-2-3\", \"Yes\"),\n            (\"gop-wins-popular-vote-by-3-4\", \"Yes\"),\n            (\"gop-wins-popular-vote-by-4-5\", \"Yes\"),\n            (\"gop-wins-popular-vote-by-5-6\", \"Yes\"),\n            (\"gop-wins-popular-vote-by-6-7\", \"Yes\"),\n            (\"gop-wins-popular-vote-by-more-than-7\", \"Yes\"),\n\n        ],\n        \"method\": \"balanced\"\n    },\n\n    {\n    \"trade_name\": \"DEM popular vote all no\",\n    \"subtitle\": \"bet no on all DEM popular vote positions\",\n    \"positions\": [\n        (\"democrats-win-popular-vote-by-over-7\", \"No\"),\n        (\"democrats-win-popular-vote-by-6-7\", \"No\"),\n        (\"democrats-win-popular-vote-by-5-6\", \"No\"),\n        (\"democrats-win-popular-vote-by-4-5\", \"No\"),\n        (\"democrats-win-popular-vote-by-3-4\", \"No\"),\n        (\"democrats-win-popular-vote-by-2-3\", \"No\"),\n        (\"democrats-win-popular-vote-by-1-2\", \"No\"),\n        (\"democrats-win-popular-vote-by-0-1\", \"No\"),\n\n    ],\n    \"method\": \"all_no\"\n    },\n\n    {\n    \"trade_name\": \"GOP popular vote all no\",\n    \"subtitle\": \"bet no on all GOP popular vote positions\",\n    \"positions\": [\n        (\"gop-wins-popular-vote-by-more-than-7\", \"No\"),\n        (\"gop-wins-popular-vote-by-6-7\", \"No\"),\n        (\"gop-wins-popular-vote-by-5-6\", \"No\"),\n        (\"gop-wins-popular-vote-by-4-5\", \"No\"),\n        (\"gop-wins-popular-vote-by-3-4\", \"No\"),\n        (\"gop-wins-popular-vote-by-2-3\", \"No\"),\n        (\"gop-wins-popular-vote-by-1-2\", \"No\"),\n        (\"gop-wins-popular-vote-by-0-1\", \"No\"),\n    ],\n    \"method\": \"all_no\"\n    },\n\n\n\n    {\n    \"trade_name\": \"Trump for president and all DEM electoral college margins\",\n    \"subtitle\" : \"This trade assumes Trump winning is the opposite of the DEM winning every category \"\n                 \"in the electoral college. The risk is Trump could be replaced or die causing one \"\n                 \"side of this trade to not work.\",\n    \"side_a_trades\": [\n        (\"2024-presidential-election-democrats-win-by-0-4\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-5-14\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-15-34\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-35-64\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-65-104\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-105-154\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-155-214\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-215\", \"Yes\"),\n    ],\n    \"side_b_trades\": [\n        (\"will-donald-trump-win-the-2024-us-presidential-election\", \"Yes\"),\n    ],\n    \"method\": \"balanced\"\n\n    },\n\n\n\n    {\n    \"trade_name\": \"Electoral College All GOP ALL DEM YES\",\n    \"subtitle\": \"This is a truely hedged trade. Bet all REP electoral college slots and bet all\"\n                \"DEM electoral college slots\",\n    \"side_a_trades\": [\n        (\"2024-presidential-election-gop-wins-by-1-4\", \"Yes\"),\n        (\"2024-presidential-election-gop-wins-by-5-14\", \"Yes\"),\n        (\"2024-presidential-election-gop-wins-by-15-34\", \"Yes\"),\n        (\"2024-presidential-election-gop-wins-by-35-64\", \"Yes\"),\n        (\"2024-presidential-election-gop-wins-by-65-104\", \"Yes\"),\n        (\"2024-presidential-election-gop-wins-by-105-154\", \"Yes\"),\n        (\"2024-presidential-election-gop-wins-by-155-214\", \"Yes\"),\n        (\"2024-presidential-election-gop-wins-by-215\", \"Yes\"),\n    ],\n    \"side_b_trades\": [\n        (\"2024-presidential-election-democrats-win-by-0-4\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-5-14\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-15-34\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-35-64\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-65-104\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-105-154\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-155-214\", \"Yes\"),\n        (\"2024-presidential-election-democrats-win-by-215\", \"Yes\"),\n    ],\n    \"method\": \"balanced\"\n    },\n\n    {\n    \"trade_name\": \"DEM electoral college all no\",\n    \"subtitle\": \"bet no on all DEM electoral college positions\",\n    \"positions\": [\n        (\"2024-presidential-election-democrats-win-by-0-4\", \"No\"),\n        (\"2024-presidential-election-democrats-win-by-5-14\", \"No\"),\n        (\"2024-presidential-election-democrats-win-by-15-34\", \"No\"),\n        (\"2024-presidential-election-democrats-win-by-35-64\", \"No\"),\n        (\"2024-presidential-election-democrats-win-by-65-104\", \"No\"),\n        (\"2024-presidential-election-democrats-win-by-105-154\", \"No\"),\n        (\"2024-presidential-election-democrats-win-by-155-214\", \"No\"),\n        (\"2024-presidential-election-democrats-win-by-215\", \"No\"),\n    ],\n    \"method\": \"all_no\"\n    },\n\n    {\n    \"trade_name\": \"REP electoral college all no\",\n    \"subtitle\": \"bet no on all REP electoral college positions\",\n    \"positions\": [\n        (\"2024-presidential-election-gop-wins-by-1-4\", \"No\"),\n        (\"2024-presidential-election-gop-wins-by-5-14\", \"No\"),\n        (\"2024-presidential-election-gop-wins-by-15-34\", \"No\"),\n        (\"2024-presidential-election-gop-wins-by-35-64\", \"No\"),\n        (\"2024-presidential-election-gop-wins-by-65-104\", \"No\"),\n        (\"2024-presidential-election-gop-wins-by-105-154\", \"No\"),\n        (\"2024-presidential-election-gop-wins-by-155-214\", \"No\"),\n        (\"2024-presidential-election-gop-wins-by-215\", \"No\"),\n        ],\n    \"method\": \"all_no\"\n    },\n\n    {\n        \"trade_name\": \"FED Rates in Sept all no\",\n        \"subtitle\": \"bet no on all FED possibilities in Sept\",\n        \"positions\": [\n            (\"fed-decreases-interest-rates-by-50-bps-after-september-2024-meeting\", \"No\"),\n            (\"fed-decreases-interest-rates-by-25-bps-after-september-2024-meeting\", \"No\"),\n            (\"no-change-in-fed-interest-rates-after-2024-september-meeting\", \"No\"),\n        ],\n        \"method\": \"all_no\"\n    },\n\n    {\n        \"trade_name\": \"Balance of power all no\",\n        \"subtitle\": \"bet no on some of the balance of power outcomes\",\n        \"positions\": [\n            (\"2024-balance-of-power-r-prez-r-senate-r-house\", \"No\"),\n            (\"2024-election-democratic-presidency-and-house-republican-senate\", \"No\"),\n            (\"democratic-sweep-in-2024-election\", \"No\"),\n            (\"2024-balance-of-power-republican-presidency-and-senate-democratic-house\", \"No\"),\n        ],\n        \"method\": \"all_no\"\n    },\n\n    {\n    \"trade_name\": \"Presidential party D President Trump\",\n    \"subtitle\": \"Bet on the candidate to win and their party to lose hedging the bet\",\n    \"side_a_trades\": [\n        (\"which-party-will-win-the-2024-united-states-presidential-election\", \"Democratic\"),\n        ],\n    \"side_b_trades\": [\n        (\"will-kamala-harris-win-the-2024-us-presidential-election\", \"No\"),\n\n        ],\n    \"method\": \"balanced\"\n    },\n\n    {\n        \"trade_name\": \"Presidential party R President Harris\",\n        \"subtitle\": \"Bet on the candidate to win and their party to lose hedging the bet\",\n        \"side_a_trades\": [\n            (\"which-party-will-win-the-2024-united-states-presidential-election\", \"Republican\"),\n        ],\n        \"side_b_trades\": [\n            (\"will-kamala-harris-win-the-2024-us-presidential-election\", \"Yes\"),\n\n        ],\n        \"method\": \"balanced\"\n    },\n\n\n    # {\n    #     \"trade_name\": \"Trump Vance ticket and vance replaced\",\n    #     \"subtitle\": \"Betting on the trump and vance ticket as well as vanced replaced\",\n    #     \"side_a_trades\": [\n    #         (\"will-trump-vance-be-gop-ticket-on-election-day\", \"Yes\"),\n    #     ],\n    #     \"side_b_trades\": [\n    #         (\"jd-vance-steps-down-as-republican-vp-nominee\", \"Yes\"),\n    #\n    #     ],\n    #     \"method\": \"balanced\"\n    # },\n\n    {\n        \"trade_name\": \"Other DEM wins election no and DEM wins election Yes\",\n        \"subtitle\": \"Taking trade on other dem besides biden to win but then saying DEMS win Presidency\",\n        \"side_a_trades\": [\n            (\"democrat-other-than-biden-wins-the-presidential-election\", \"No\"),\n        ],\n        \"side_b_trades\": [\n            (\"which-party-will-win-the-2024-united-states-presidential-election\", \"Democratic\"),\n\n        ],\n        \"method\": \"balanced\"\n    },\n\n    {\n        \"trade_name\": \"538 call election no and buy predicted candidate\",\n        \"subtitle\": \"538 will probably call the election correctly as indicated by the numbers. But if the day before\"\n                    \"the election as long as you buy the same number of shares as the winning candidate cheaper than\"\n                    \"the inverse of the price you paid for these shares you will profit\",\n        \"side_a_trades\": [\n            (\"will-538-correctly-call-the-presidential-election\", \"No\"),\n        ],\n        \"side_b_trades\": [\n            (\"which-party-will-win-the-2024-united-states-presidential-election\", \"Republican\"),\n\n        ],\n        \"method\": \"balanced\"\n    },\n\n\n    {\n        \"trade_name\": \"Nate Silver call election no and buy Nates predicted candidate\",\n        \"subtitle\": \"Nate Silver will probably call the election correctly as indicated by the numbers. But if the day before\"\n                    \"the election as long as you buy the same number of shares as the winning candidate cheaper than\"\n                    \"the inverse of the price you paid for these shares you will profit\",\n        \"side_a_trades\": [\n            (\"will-nate-silver-correctly-call-the-presidential-election\", \"No\"),\n        ],\n        \"side_b_trades\": [\n            (\"which-party-will-win-the-2024-united-states-presidential-election\", \"Republican\"),\n\n        ],\n        \"method\": \"balanced\"\n    },\n\n    # {\n    #     \"trade_name\": \"DEM solid red No\",\n    #     \"subtitle\": \"The main state the DEMS have a chance in is OH. So the opposite of DEMS winning a\"\n    #                 \"solid red state, no is the DEMS actually winning the most probable state OH(10.5%)\"\n    #                 \". All of the rest of these states carry a &lt; 10% chance&quot;,\n    #     &quot;side_a_trades&quot;: [\n    #         (&quot;us-presidential-election-democrats-win-a-solid-red-state&quot;, &quot;No&quot;),\n    #     ],\n    #     &quot;side_b_trades&quot;:\n    #         [\n    #             # (&quot;will-a-democrat-win-alabama-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-alaska-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-arkansas-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-idaho-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-indiana-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-iowa-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-kansas-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-kentucky-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-louisiana-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-mississippi-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-missouri-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-montana-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-nebraska-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-north-dakota-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             (&quot;will-a-democrat-win-ohio-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-oklahoma-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-south-carolina-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-south-dakota-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-tennessee-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-utah-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-west-virginia-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-democrat-win-wyoming-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;)\n    #         ],\n    #     &quot;method&quot;: &quot;balanced&quot;\n    #\n    # },\n\n    # {\n    #     &quot;trade_name&quot;: &quot;REP solid blue No&quot;,\n    #     &quot;subtitle&quot;: &quot;The main state the REPS have a chance in is VA. So the opposite of REPS winning a&quot;\n    #                 &quot;solid blue state, no is the REPS actually winning the most probable state VA(15%). All&quot;\n    #                 &quot;of the rest of these states carry a &lt; 10% chance&quot;,\n    #     &quot;side_a_trades&quot;: [\n    #         (&quot;presidential-election-republicans-win-a-solid-blue-state&quot;, &quot;No&quot;),\n    #     ],\n    #     &quot;side_b_trades&quot;:\n    #         [\n    #             # (&quot;will-a-republican-win-california-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-colorado-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-connecticut-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-delaware-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-hawaii-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-illinois-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-maryland-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-massachusetts-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-new-jersey-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-new-mexico-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-new-york-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-oregon-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-rhode-island-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-republican-win-vermont-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             (&quot;will-a-republican-win-virginia-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;),\n    #             # (&quot;will-a-reoublican-win-washington-in-the-2024-us-presidential-election&quot;, &quot;Yes&quot;)\n    #         ],\n    #     &quot;method&quot;: &quot;balanced&quot;\n    #\n    # },\n\n\n    # {\n    #     &quot;trade_name&quot;: &quot;Trump wins every swing state yes&quot;,\n    #     &quot;subtitle&quot;: &quot;This trade bets that trump will win every swing state(cheap). Then it bets that &quot;\n    #                 &quot;harris will win MI(swing state). Harris is most favored to win MI 46.5\/43.6 by &quot;\n    #                 &quot;Nate Silver and 61% on Polymarket. The theory here is if Trump wins MI then he&quot;\n    #                 &quot;should also win the other swing states given that this is the largest margin state.&quot;,\n    #     &quot;side_a_trades&quot;: [\n    #         (&quot;trump-wins-every-swing-state&quot;, &quot;Yes&quot;),\n    #     ],\n    #     &quot;side_b_trades&quot;: [  # (&quot;will-a-democrat-win-arizona-presidential-election&quot;, &quot;Yes&quot;),\n    #         # (&quot;will-a-democrat-win-georgia-presidential-election&quot;, &quot;Yes&quot;),\n    #         (&quot;will-a-democrat-win-michigan-presidential-election&quot;, &quot;Yes&quot;),\n    #         # (&quot;will-a-democrat-win-nevada-presidential-election&quot;, &quot;Yes&quot;),\n    #         # (&quot;will-a-democrat-win-north-carolina-presidential-election&quot;, &quot;Yes&quot;),\n    #         # (&quot;will-a-democrat-win-pennsylvania-presidential-election&quot;, &quot;Yes&quot;),\n    #         # (&quot;will-a-democrat-win-wisconsin-presidential-election&quot;, &quot;Yes&quot;),\n    #\n    #     ],\n    #     &quot;method&quot;: &quot;balanced&quot;\n    #\n    # },\n\n\n    # {\n    # &quot;trade_name&quot;: &quot;Harris wins every swing state yes&quot;,\n    # &quot;subtitle&quot;: &quot;This trade bets that Harris will win every swing state(cheap). Then it bets that &quot;\n    #             &quot;Trump will win GA(swing state). Trump is most favored to win GA 46.8\/45.3 by &quot;\n    #             &quot;Nate Silver and 61% on Polymarket. The theory here is if Harris wins GA then she&quot;\n    #             &quot;should also win the other swing states given that this is the largest margin state.&quot;,            &quot;side_a_trades&quot;: [\n    #     (&quot;will-kamala-harris-win-every-swing-state&quot;, &quot;Yes&quot;),\n    # ],\n    # &quot;side_b_trades&quot;: [\n    #     # (&quot;will-a-democrat-win-arizona-presidential-election&quot;, &quot;No&quot;),\n    #     (&quot;will-a-democrat-win-georgia-presidential-election&quot;, &quot;No&quot;),\n    #     # (&quot;will-a-democrat-win-michigan-presidential-election&quot;, &quot;No&quot;),\n    #     # (&quot;will-a-democrat-win-nevada-presidential-election&quot;, &quot;No&quot;),\n    #     # (&quot;will-a-democrat-win-pennsylvania-presidential-election&quot;, &quot;No&quot;),\n    #     # (&quot;will-a-democrat-win-wisconsin-presidential-election&quot;, &quot;No&quot;),\n    # ],\n    # &quot;method&quot;: &quot;balanced&quot;\n    #\n    # },\n    #\n    # {\n    #     &quot;trade_name&quot;: &quot;REP flip Biden State&quot;,\n    #     &quot;subtitle&quot;: &quot;test&quot;,\n    #     &quot;side_a_trades&quot;: [\n    #         (&quot;republicans-flip-a-2020-biden-state&quot;, &quot;No&quot;),\n    #     ],\n    #     &quot;side_b_trades&quot;: [\n    #         (&quot;will-a-democrat-win-georgia-presidential-election&quot;, &quot;No&quot;),\n    #     ],\n    #     &quot;method&quot;: &quot;balanced&quot;\n    #\n    # },\n    # {\n    #     &quot;trade_name&quot;: &quot;DEM flip Trump State&quot;,\n    #     &quot;subtitle&quot;: &quot;test&quot;,\n    #     &quot;side_a_trades&quot;: [\n    #         (&quot;dems-flip-a-2020-trump-state&quot;, &quot;No&quot;),\n    #     ],\n    #     &quot;side_b_trades&quot;: [\n    #         (&quot;will-a-democrat-win-north-carolina-presidential-election&quot;, &quot;Yes&quot;),\n    #     ],\n    #     &quot;method&quot;: &quot;balanced&quot;\n    # },\n\n    ]<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">get_market_book_and_live_arb.py<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This program will take your strategies and generate a nice HTML file that will constantly update the arbitrage opportunity. The output looks like this.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11897,\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" width=\"940\" height=\"1024\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-14-940x1024.png\" alt=\"\" class=\"wp-image-11897 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-14-940x1024.png 940w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-14-275x300.png 275w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-14-768x837.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-14.png 1103w\" data-sizes=\"(max-width: 940px) 100vw, 940px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 940px; --smush-placeholder-aspect-ratio: 940\/1024;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport sys\nimport json\nimport pandas as pd\nimport logging\nimport time\nfrom datetime import datetime\nimport pytz\nfrom py_clob_client.client import ClobClient\nfrom strategies import trades\nfrom get_order_book import update_books_for_trades  # Import the function\nfrom dotenv import load_dotenv\nimport numpy as np\nfrom get_live_price import get_live_price  # Import the new live price function\nimport jinja2\nimport tempfile\nimport numpy as np\nimport subprocess\n\n# Access the environment variables\napi_key = os.getenv('API_KEY')\n\n# Set up logging configuration\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n\n# Replace with your actual host and chain ID\nhost = \"https:\/\/clob.polymarket.com\"\nchain_id = 137  # Polygon Mainnet\n\n# Initialize the ClobClient\nclient = ClobClient(host, key=api_key, chain_id=chain_id)\n\n# Dictionary to cache live prices\nlive_price_cache = {}\n\n# Load environment variables\nload_dotenv()\n\ndef load_market_lookup():\n    with open('.\/data\/market_lookup.json', 'r') as f:\n        market_lookup = json.load(f)\n\n    slug_to_token_id = {}\n    for market in market_lookup.values():\n        slug = market['market_slug']\n        slug_to_token_id[slug] = {token['outcome']: token['token_id'] for token in market['tokens']}\n\n    return slug_to_token_id\n\ndef get_actual_price(slug, outcome, user_id='JeremyRWhittaker'):\n    \"\"\"\n    Get the actual price and size from the user's latest trade for the specified slug and outcome.\n    \"\"\"\n    file_path = f'.\/data\/user_trades\/{user_id}_enriched_transactions.parquet'\n    if not os.path.exists(file_path):\n        logging.warning(f\"User trades file not found: {file_path}\")\n        return None, None\n    try:\n        df = pd.read_parquet(file_path)\n    except Exception as e:\n        logging.error(f\"Failed to read user trades file: {file_path}. Error: {e}\")\n        return None, None\n\n    # Parse 'timeStamp_erc1155' as datetime\n    try:\n        df['timeStamp_erc1155'] = pd.to_datetime(df['timeStamp_erc1155'])\n    except Exception as e:\n        logging.error(f\"Failed to parse 'timeStamp_erc1155' as datetime: {e}\")\n        return None, None\n\n    # Filter by market_slug and outcome\n    df_filtered = df[(df['market_slug'] == slug) &amp; (df['outcome'] == outcome)]\n    if df_filtered.empty:\n        logging.info(f\"No trades found for user {user_id} in market {slug} ({outcome})\")\n        return None, None\n\n    # Get the row with the latest 'timeStamp_erc1155'\n    latest_trade = df_filtered.loc[df_filtered['timeStamp_erc1155'].idxmax()]\n    price = latest_trade['price_paid_per_token']\n    size = latest_trade['shares']\n\n    return price, size\n\ndef get_price_and_size(df, price_type):\n    if price_type == 'ask':\n        relevant_df = df[df['side'] == 'ask']\n        if not relevant_df.empty:\n            min_price_row = relevant_df.loc[relevant_df['price'].idxmin()]\n            return min_price_row['price'], min_price_row['size']\n    elif price_type == 'bid':\n        relevant_df = df[df['side'] == 'bid']\n        if not relevant_df.empty:\n            max_price_row = relevant_df.loc[relevant_df['price'].idxmax()]\n            return max_price_row['price'], max_price_row['size']\n    elif price_type == 'mid':\n        ask_df = df[df['side'] == 'ask']\n        bid_df = df[df['side'] == 'bid']\n        if not ask_df.empty and not bid_df.empty:\n            min_ask_price = ask_df['price'].min()\n            max_bid_price = bid_df['price'].max()\n            return (min_ask_price + max_bid_price) \/ 2, None\n    return None, None\n\ndef get_live_price(token_id, side):\n    cache_key = f\"{token_id}_{side.upper()}\"\n    current_time = time.time()  # Get the current time in seconds since the Epoch\n\n    # Check if the price is in the cache and if it's still valid (not older than 2 minutes)\n    if cache_key in live_price_cache:\n        cached_price, timestamp = live_price_cache[cache_key]\n        if current_time - timestamp  0:\n                    profit = 1 - total_cost\n                    arb_pct = profit * 100\n                    logging.info(f\"Total cost: {total_cost:.4f}, profit: {profit:.4f}, arb: {arb_pct:.4f}%\")\n                    arbitrage_per_price_type[price_type] = arb_pct\n                else:\n                    logging.warning(f\"Data incomplete for trade: {trade_name}, setting arbitrage to NaN for {price_type}.\")\n                    arbitrage_per_price_type[price_type] = np.nan\n\n        if arbitrage_per_price_type:\n            arbitrage_info[trade_name] = arbitrage_per_price_type\n            logging.info(f\"\\nArbitrage opportunity for {trade_name}: {arbitrage_info[trade_name]}\")\n        else:\n            logging.info(f\"No arbitrage opportunities found for {trade_name}.\")\n\n    return arbitrage_info\ndef get_spread_from_api(slug, outcome, slug_to_token_id):\n    token_id = slug_to_token_id.get(slug, {}).get(outcome)\n    if not token_id:\n        logging.warning(f\"Token ID not found for {slug} ({outcome})\")\n        return None\n\n    try:\n        spread_info = client.get_spread(token_id=token_id)\n        if spread_info and 'spread' in spread_info:\n            try:\n                spread_value = float(spread_info['spread'])\n                return spread_value\n            except ValueError:\n                logging.error(f\"Spread value is not a float: {spread_info['spread']}\")\n                return None\n    except Exception as e:\n        logging.error(f\"Failed to fetch spread for token {token_id}: {str(e)}\")\n    return None\n\n\ndef process_all_trades(trades, output_dir='.\/strategies', include_bid=True):\n    \"\"\"\n    Process all trades, saving the results to CSV and HTML.\n    \"\"\"\n    if not os.path.exists(output_dir):\n        os.makedirs(output_dir)\n\n    # Default user ID\n    user_id = 'JeremyRWhittaker'\n\n    # Run get_user_trade_prices.py as a subprocess with --run-once\n    try:\n        logging.info(f\"Updating user trades for user: {user_id}\")\n        subprocess.run(\n            ['python', 'get_user_trade_prices.py', user_id, '.\/data\/strategies.py'],\n            check=True\n        )\n        logging.info(f\"Successfully updated user trades for user: {user_id}\")\n    except subprocess.CalledProcessError as e:\n        logging.error(f\"Error updating user trades: {e}\")\n        return  # Exit the function if updating trades is critical\n    if not os.path.exists(output_dir):\n        os.makedirs(output_dir)\n\n    slug_to_token_id = load_market_lookup()\n    arbitrage_info = {}\n    datasets = {}\n    spread_info = {}\n    trade_descriptions = {}\n\n    # Determine the price types based on the include_bid flag\n    price_types = ['ask', 'mid', 'live', 'actual']\n    if include_bid:\n        price_types.append('bid')\n\n    user_id = 'JeremyRWhittaker'  # Default user ID\n\n    for trade in trades:\n        trade_name = trade['trade_name']\n        trade_descriptions[trade_name] = trade.get('description', '')  # Store the description\n        trade_datasets = {}\n        trade_spreads = {}\n\n        if trade['method'] == 'all_no':\n            for price_type in price_types:\n                save_trade_details_with_prices(trade, ['positions'], price_type, output_dir, slug_to_token_id, user_id)\n                # Load and store dataset\n                dataset_path = os.path.join(output_dir, f\"{trade_name}_{price_type}.csv\")\n                if os.path.exists(dataset_path):\n                    trade_datasets[price_type] = pd.read_csv(dataset_path)\n        elif trade['method'] == 'balanced':\n            for price_type in price_types:\n                save_trade_details_with_prices(trade, ['side_a_trades', 'side_b_trades'], price_type, output_dir,\n                                               slug_to_token_id, user_id)\n                # Load and store dataset\n                dataset_path = os.path.join(output_dir, f\"{trade_name}_{price_type}.csv\")\n                if os.path.exists(dataset_path):\n                    trade_datasets[price_type] = pd.read_csv(dataset_path)\n\n        # Get the spread for the trade using the token ID\n        for side in ['positions', 'side_a_trades', 'side_b_trades']:\n            trade_sides = trade.get(side, [])\n            for slug, outcome in trade_sides:\n                try:\n                    logging.info(f\"Processing trade side {side} for slug: {slug} and outcome: {outcome}\")\n                    token_id = slug_to_token_id.get(slug, {}).get(outcome)\n                    if token_id:\n                        logging.info(f\"Token ID found for {slug} ({outcome}): {token_id}\")\n                        spread = get_spread_from_api(slug, outcome, slug_to_token_id)\n                        if spread is not None:\n                            trade_spreads[slug] = spread\n                            logging.info(f\"Spread for {slug} ({outcome}): {spread}\")\n                        else:\n                            logging.warning(f\"No spread found for {slug} ({outcome})\")\n                    else:\n                        logging.warning(f\"Token ID not found for {slug} ({outcome})\")\n                except Exception as e:\n                    logging.error(f\"Error processing trade side {side} for {slug} ({outcome}): {e}\", exc_info=True)\n\n        # Store the datasets and spreads\n        try:\n            logging.info(f\"Storing datasets and spread information for trade: {trade_name}\")\n            datasets[trade_name] = trade_datasets\n            spread_info[trade_name] = trade_spreads\n        except Exception as e:\n            logging.error(f\"Error storing datasets and spreads for trade: {trade_name}: {e}\", exc_info=True)\n\n    # Calculate arbitrage opportunities for all trades at once\n    try:\n        logging.info(\"Calculating arbitrage for all trades\")\n        arbitrage_info = calculate_arbitrage_for_scenarios(trades, price_types=price_types, user_id=user_id)\n    except Exception as e:\n        logging.error(f\"Error calculating arbitrage: {e}\", exc_info=True)\n\n    # Save summary and datasets to HTML, including trade descriptions\n    try:\n        logging.info(\"Saving summary and datasets to HTML\")\n        save_summary_to_html_with_datasets(arbitrage_info, datasets, spread_info, output_dir, trade_descriptions)\n    except Exception as e:\n        logging.error(f\"Error saving summary and datasets to HTML: {e}\", exc_info=True)\n\n    # Optionally save to CSV as well\n    try:\n        logging.info(\"Saving summary to CSV\")\n        save_summary_to_csv(arbitrage_info, output_dir, datasets)\n    except Exception as e:\n        logging.error(f\"Error saving summary to CSV: {e}\", exc_info=True)\n\ndef save_summary_to_csv(arbitrage_info, output_dir, datasets):\n    \"\"\"\n    Save a summary of arbitrage opportunities to a CSV file.\n    \"\"\"\n    summary_data = []\n\n    # Populate summary data\n    for trade_name, arb_data in arbitrage_info.items():\n        for price_type, arb_value in arb_data.items():\n            summary_data.append({\n                'Trade Name': trade_name,\n                'Price Type': price_type,\n                'Arbitrage %': arb_value\n            })\n\n    if summary_data:\n        df_summary = pd.DataFrame(summary_data)\n\n        # Sort by Arbitrage %\n        df_summary.sort_values(by=['Arbitrage %'], ascending=False, inplace=True)\n    else:\n        # If no data is found, create an empty DataFrame with appropriate columns\n        df_summary = pd.DataFrame(columns=['Trade Name', 'Price Type', 'Arbitrage %'])\n\n    summary_path = os.path.join(output_dir, \"summary.csv\")\n    df_summary.to_csv(summary_path, index=False)\n    logging.info(\"Summary results exported to %s\", summary_path)\ndef save_summary_to_html_with_datasets(arbitrage_info, datasets, spread_info, output_dir, trade_descriptions):\n    \"\"\"\n    Save a summary of arbitrage opportunities to an HTML file using Jinja2 templates,\n    with links to the corresponding detailed datasets.\n    \"\"\"\n    import numpy as np  # Ensure numpy is imported\n\n    # Get the current time in Arizona time zone\n    arizona_tz = pytz.timezone('America\/Phoenix')\n    run_time = datetime.now(arizona_tz).strftime('%Y-%m-%d %H:%M:%S %Z')\n\n    # Prepare data to be passed to the template\n    trades_summary = []\n    trades_list = []\n\n    for trade_name, arb_data in arbitrage_info.items():\n        # Get the description\n        description = trade_descriptions.get(trade_name, '')\n        # Remove the spreads from the trade name\n        trade_name_with_spread = trade_name  # Not including spreads\n\n        # Prepare list of price types and arbitrage values for this trade\n        price_types_data = []\n        ask_arbitrage_num = None  # Initialize to store ask arbitrage value\n\n        for price_type, arb_value in arb_data.items():\n            link_id = f\"{trade_name.replace(' ', '_').replace('\/', '-')}_{price_type}\"\n            try:\n                arb_num = float(arb_value)\n                if np.isnan(arb_num):\n                    arb_num = float('-inf')\n                    arb_str = 'NaN'\n                else:\n                    arb_str = f\"{arb_num:.2f}\"\n            except (TypeError, ValueError):\n                arb_num = float('-inf')\n                arb_str = 'NaN'\n\n            price_types_data.append({\n                'price_type': price_type,\n                'arbitrage': arb_str,\n                'arbitrage_num': arb_num,\n                'link_id': link_id\n            })\n\n            # Store ask arbitrage value\n            if price_type == 'ask':\n                ask_arbitrage_num = arb_num\n\n            # Get the dataset HTML\n            dataset = datasets.get(trade_name, {}).get(price_type)\n            if dataset is not None:\n                dataset_html = dataset.to_html(index=False)\n            else:\n                dataset_html = \"<p>No data available for this trade and price type.<\/p>\"\n\n            trades_list.append({\n                'trade_name': trade_name_with_spread,\n                'price_type': price_type,\n                'link_id': link_id,\n                'dataset_html': dataset_html\n            })\n\n        # Sort price_types_data, ensure 'ask' is first\n        price_types_data.sort(key=lambda x: 0 if x['price_type'] == 'ask' else 1)\n\n        # Add ask_arbitrage_num to trade data\n        trades_summary.append({\n            'trade_name': trade_name_with_spread,\n            'description': description,\n            'price_types': price_types_data,\n            'ask_arbitrage_num': ask_arbitrage_num\n        })\n\n    # Now sort trades_summary by 'ask_arbitrage_num' descending, handling NaN values\n    def sort_key(x):\n        ask_arb = x.get('ask_arbitrage_num')\n        if ask_arb is None or np.isnan(ask_arb):\n            return float('-inf')  # Treat NaN and None as the lowest value\n        else:\n            return ask_arb\n\n    trades_summary.sort(key=sort_key, reverse=True)\n\n    # Prepare context for the template\n    context = {\n        'run_time': run_time,\n        'trades_summary': trades_summary,\n        'trades': trades_list\n    }\n\n    # Load the Jinja2 template with CSS styling\n    html_template = \"\"\"\n    \n    \n    \n        \n        \n        \n        <title>Arbitrage Summary<\/title>\n        \n            body {\n                font-family: Arial, sans-serif;\n                margin: 20px;\n                background-color: #f9f9f9;\n            }\n            h1, h2, h3 {\n                color: #333;\n            }\n            table {\n                width: 100%;\n                border-collapse: collapse;\n                margin-bottom: 20px;\n                background-color: #fff;\n            }\n            th, td {\n                padding: 12px;\n                border: 1px solid #ddd;\n                text-align: left;\n            }\n            th {\n                background-color: #f4f4f4;\n            }\n            .arb-positive {\n                background-color: #d4edda !important; \/* Light green *\/\n            }\n            a {\n                color: #3498db;\n                text-decoration: none;\n            }\n            a:hover {\n                text-decoration: underline;\n            }\n            .timestamp {\n                font-size: 0.9em;\n                color: #777;\n            }\n            .trade-section {\n                margin-bottom: 30px;\n                padding-bottom: 10px;\n                border-bottom: 1px solid #ccc;\n            }\n            .trade-description {\n                font-style: italic;\n                margin-bottom: 10px;\n            }\n        \n    \n    \n        <h1>Arbitrage Summary<\/h1>\n        <p class=\"timestamp\">Updated at: {{ run_time }}<\/p>\n\n        {% for trade in trades_summary %}\n        <div class=\"trade-section\">\n            <h2>{{ trade.trade_name }}<\/h2>\n            {% if trade.description %}\n            <p class=\"trade-description\">{{ trade.description }}<\/p>\n            {% endif %}\n            <table>\n                <thead>\n                    <tr>\n                        <th>Price Type<\/th>\n                        <th>Arbitrage %<\/th>\n                    <\/tr>\n                <\/thead>\n                <tbody>\n                    {% for item in trade.price_types %}\n                    <tr> 0 %}arb-positive{% endif %}\"&gt;\n                        <td><a href=\"#{{ item.link_id }}\">{{ item.price_type }}<\/a><\/td>\n                        <td>{{ item.arbitrage }}<\/td>\n                    <\/tr>\n                    {% endfor %}\n                <\/tbody>\n            <\/table>\n        <\/div>\n        {% endfor %}\n\n        {% for trade in trades %}\n        <h2 id=\"{{ trade.link_id }}\">{{ trade.trade_name }} ({{ trade.price_type }})<\/h2>\n        {{ trade.dataset_html | safe }}\n        {% endfor %}\n\n        \n            \/\/ Auto-reload the page every 30 seconds to reflect new updates\n            setInterval(function() {\n                window.location.reload(true);  \/\/ Force reload without using the cache\n            }, 30000);  \/\/ Reload every 30 seconds\n        \n    \n    \n    \"\"\"\n\n    # Create a Jinja2 environment and render the template\n    template = jinja2.Template(html_template)\n    rendered_html = template.render(context)\n\n    # Write to a temporary file and then replace the original file atomically\n    summary_html_path = os.path.join(output_dir, \"summary.html\")\n\n    import tempfile\n    try:\n        # Write to a temporary file\n        with tempfile.NamedTemporaryFile('w', delete=False, dir=output_dir, prefix='summary_', suffix='.html') as tmp_file:\n            tmp_file.write(rendered_html)\n            temp_file_path = tmp_file.name\n\n        # Atomically replace the old file with the new file\n        os.replace(temp_file_path, summary_html_path)\n        logging.info(\"Summary with datasets exported to %s\", summary_html_path)\n    except Exception as e:\n        logging.error(f\"Error saving summary HTML file: {e}\", exc_info=True)\n\n\n\ndef run_continuously(trades, output_dir='.\/strategies', include_bid=True, interval=300):\n    \"\"\"\n    Run the process_all_trades function every 'interval' seconds.\n    \"\"\"\n    while True:\n        try:\n            # First, update the order books\n            logging.info(\"Updating order books before processing trades.\")\n            update_books_for_trades()\n\n            # Run the main processing function\n            process_all_trades(trades, output_dir=output_dir, include_bid=include_bid)\n\n            # Log the completion of one iteration\n            logging.info(\"Completed one iteration of process_all_trades.\")\n\n            # Sleep for the specified interval (300 seconds = 5 minutes)\n            time.sleep(interval)\n\n        except Exception as e:\n            logging.error(f\"An error occurred: {e}\")\n            # Sleep for a bit before trying again in case of error\n            time.sleep(interval)\n\n# Example usage:\nif __name__ == \"__main__\":\n    run_continuously(trades, include_bid=True)\n<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">get_polygon_data.py<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>With this code, you can actually put in any username or wallet id and it will generate charts and graphs of that user&#8217;s trade history. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11898,\"sizeSlug\":\"large\",\"linkDestination\":\"media\",\"className\":\"is-style-default\"} -->\n<figure class=\"wp-block-image size-large is-style-default\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-34.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-34-1024x771.png\" alt=\"\" class=\"wp-image-11898 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-34-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-34-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-34-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-34-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-34-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-34.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11899,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-33.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-33-1024x771.png\" alt=\"\" class=\"wp-image-11899 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-33-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-33-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-33-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-33-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-33-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-33.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11900,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-32.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-32-1024x771.png\" alt=\"\" class=\"wp-image-11900 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-32-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-32-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-32-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-32-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-32-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-32.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11901,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-31.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-31-1024x771.png\" alt=\"\" class=\"wp-image-11901 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-31-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-31-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-31-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-31-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-31-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-31.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11902,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-30.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-30-1024x771.png\" alt=\"\" class=\"wp-image-11902 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-30-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-30-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-30-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-30-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-30-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-30.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11903,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-29.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-29-1024x771.png\" alt=\"\" class=\"wp-image-11903 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-29-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-29-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-29-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-29-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-29-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-29.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:image {\"id\":11904,\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-28.png\"><img decoding=\"async\" width=\"1024\" height=\"771\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-28-1024x771.png\" alt=\"\" class=\"wp-image-11904 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-28-1024x771.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-28-300x226.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-28-768x578.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-28-1536x1156.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-28-399x300.png 399w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-28.png 1702w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/771;\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport requests\nimport logging\nimport pandas as pd\nimport subprocess\nimport json\nimport time\nfrom dotenv import load_dotenv\nimport plotly.express as px\nimport re\nfrom bs4 import BeautifulSoup\nfrom importlib import reload\nimport numpy as np\nimport argparse\nimport os\nimport subprocess\nimport json\nimport logging\nimport pandas as pd\nfrom dotenv import load_dotenv\n\nlogging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')\nlogger = logging.getLogger(__name__)\n\n\n# Load environment variables\nload_dotenv(\"keys.env\")\n\nprice_cache = {}\n\n# EXCHANGES\nCTF_EXCHANGE = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'\nNEG_RISK_CTF_EXCHANGE = '0x4d97dcd97ec945f40cf65f87097ace5ea0476045'\n\n# SPENDERS FOR EXCHANGES\nNEG_RISK_CTF_EXCHANGE_SPENDER = '0xC5d563A36AE78145C45a50134d48A1215220f80a'\nNEG_RISK_ADAPTER = '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296'\nCTF_EXCHANGE_SPENDER = '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E'\n\nCACHE_EXPIRATION_TIME = 60 * 30  # Cache expiration time in seconds (5 minutes)\nPRICE_CACHE_FILE = '.\/data\/live_price_cache.json'\n\n# Dictionary to cache live prices\nlive_price_cache = {}\n\n\ndef load_price_cache():\n    \"\"\"Load the live price cache from a JSON file.\"\"\"\n    if os.path.exists(PRICE_CACHE_FILE):\n        try:\n            with open(PRICE_CACHE_FILE, 'r') as file:\n                return json.load(file)\n        except json.JSONDecodeError as e:\n            logger.error(f\"Error loading price cache: {e}\")\n            return {}\n    return {}\n\ndef save_price_cache(cache):\n    \"\"\"Save the live price cache to a JSON file.\"\"\"\n    with open(PRICE_CACHE_FILE, 'w') as file:\n        json.dump(cache, file)\n\ndef is_cache_valid(cache_entry, expiration_time=CACHE_EXPIRATION_TIME):\n    \"\"\"\n    Check if the cache entry is still valid based on the current time and expiration time.\n    \"\"\"\n    if not cache_entry:\n        return False\n    cached_time = cache_entry.get('timestamp', 0)\n    return (time.time() - cached_time) &lt; expiration_time\n\n\ndef call_get_live_price(token_id, expiration_time=CACHE_EXPIRATION_TIME):\n    &quot;&quot;&quot;\n    Get live price from cache or update it if expired.\n    &quot;&quot;&quot;\n    logger.info(f&#039;Getting live price for token {token_id}&#039;)\n\n    # Load existing cache\n    price_cache = load_price_cache()\n    cache_key = f&quot;{token_id}&quot;\n\n    # Check if cache is valid\n    if cache_key in price_cache and is_cache_valid(price_cache[cache_key], expiration_time):\n        logger.info(f&#039;Returning cached price for {cache_key}&#039;)\n        return price_cache[cache_key][&#039;price&#039;]\n\n    # If cache is expired or doesn&#039;t exist, fetch live price\n    try:\n        result = subprocess.run(\n            [&#039;python3&#039;, &#039;get_live_price.py&#039;, token_id],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            text=True,\n            check=True\n        )\n        # Parse the live price from the subprocess output\n        output_lines = result.stdout.strip().split(&quot;\\n&quot;)\n        live_price_line = next((line for line in output_lines if &quot;Live price for token&quot; in line), None)\n        if live_price_line:\n            live_price = float(live_price_line.strip().split(&quot;:&quot;)[-1].strip())\n        else:\n            logger.error(&quot;Live price not found in subprocess output.&quot;)\n            return None\n\n        logger.debug(f&quot;Subprocess get_live_price output: {result.stdout}&quot;)\n\n        # Update cache with the new price and timestamp\n        price_cache[cache_key] = {&#039;price&#039;: live_price, &#039;timestamp&#039;: time.time()}\n        save_price_cache(price_cache)\n\n        return live_price\n\n    except subprocess.CalledProcessError as e:\n        logger.error(f&quot;Subprocess get_live_price error: {e.stderr}&quot;)\n        return None\n    except Exception as e:\n        logger.error(f&quot;Error fetching live price: {str(e)}&quot;)\n        return None\n\ndef update_live_price_and_pl(merged_df, contract_token_id, market_slug=None, outcome=None):\n    &quot;&quot;&quot;\n    Calculate the live price and profit\/loss (pl) for each trade in the DataFrame.\n    &quot;&quot;&quot;\n    # Ensure tokenID in merged_df is string\n    merged_df[&#039;tokenID&#039;] = merged_df[&#039;tokenID&#039;].astype(str)\n    contract_token_id = str(contract_token_id)\n\n    # Check for NaN or empty token IDs\n    if not contract_token_id or contract_token_id == &#039;nan&#039;:\n        logger.warning(&quot;Encountered NaN or empty contract_token_id. Skipping.&quot;)\n        return merged_df\n\n    # Add live_price and pl columns if they don&#039;t exist\n    if &#039;live_price&#039; not in merged_df.columns:\n        merged_df[&#039;live_price&#039;] = np.nan\n    if &#039;pl&#039; not in merged_df.columns:\n        merged_df[&#039;pl&#039;] = np.nan\n\n    # Filter rows with the same contract_token_id and outcome\n    merged_df[&#039;outcome&#039;] = merged_df[&#039;outcome&#039;].astype(str)\n    matching_rows = merged_df[(merged_df[&#039;tokenID&#039;] == contract_token_id) &amp;\n                              (merged_df[&#039;outcome&#039;].str.lower() == outcome.lower())]\n\n    if not matching_rows.empty:\n        logger.info(f&#039;Fetching live price for token {contract_token_id}&#039;)\n        live_price = call_get_live_price(contract_token_id)\n        logger.info(f&#039;Live price for token {contract_token_id}: {live_price}&#039;)\n\n        if live_price is not None:\n            try:\n                # Calculate profit\/loss based on the live price\n                price_paid_per_token = matching_rows[&#039;price_paid_per_token&#039;]\n                total_purchase_value = matching_rows[&#039;total_purchase_value&#039;]\n                pl = ((live_price - price_paid_per_token) \/ price_paid_per_token) * total_purchase_value\n\n                # Update the DataFrame with live price and pl\n                merged_df.loc[matching_rows.index, &#039;live_price&#039;] = live_price\n                merged_df.loc[matching_rows.index, &#039;pl&#039;] = pl\n            except Exception as e:\n                logger.error(f&quot;Error calculating live price and profit\/loss: {e}&quot;)\n        else:\n            logger.warning(f&quot;Live price not found for tokenID {contract_token_id}&quot;)\n            merged_df.loc[matching_rows.index, &#039;pl&#039;] = np.nan\n\n    return merged_df\n\n\n\n\ndef find_token_id(market_slug, outcome, market_lookup):\n    &quot;&quot;&quot;Find the token_id based on market_slug and outcome.&quot;&quot;&quot;\n    for market in market_lookup.values():\n        if market[&#039;market_slug&#039;] == market_slug:\n            for token in market[&#039;tokens&#039;]:\n                if token[&#039;outcome&#039;].lower() == outcome.lower():\n                    return token[&#039;token_id&#039;]\n    return None\n\n\ndef fetch_data(url):\n    &quot;&quot;&quot;Fetch data from a given URL and return the JSON response.&quot;&quot;&quot;\n    try:\n        response = requests.get(url, timeout=10)  # You can specify a timeout\n        response.raise_for_status()  # Raise an error for bad responses (4xx, 5xx)\n        return response.json()\n    except requests.exceptions.RequestException as e:\n        logger.error(f&quot;Error fetching data from URL: {url}. Exception: {e}&quot;)\n        return None\n\ndef fetch_all_pages(api_key, token_ids, market_slug_outcome_map, csv_output_dir=&#039;.\/data\/polymarket_trades\/&#039;):\n    page = 1\n    offset = 100\n    retry_attempts = 0\n    all_data = []  # Store all data here\n\n    while True:\n        url = f&quot;https:\/\/api.polygonscan.com\/api?module=account&amp;action=token1155tx&amp;contractaddress={NEG_RISK_CTF_EXCHANGE}&amp;page={page}&amp;offset={offset}&amp;startblock=0&amp;endblock=99999999&amp;sort=desc&amp;apikey={api_key}&quot;\n        logger.info(f&quot;Fetching transaction data for tokens {token_ids}, page: {page}&quot;)\n\n        data = fetch_data(url)\n\n        if data and data[&#039;status&#039;] == &#039;1&#039;:\n            df = pd.DataFrame(data[&#039;result&#039;])\n\n            if df.empty:\n                logger.info(&quot;No more transactions found, ending pagination.&quot;)\n                break  # Exit if there are no more transactions\n\n            all_data.append(df)\n            page += 1  # Go to the next page\n        else:\n            logger.error(f&quot;API response error or no data found for page {page}&quot;)\n            if retry_attempts &lt; 5:\n                retry_attempts += 1\n                time.sleep(retry_attempts)\n            else:\n                break\n\n    if all_data:\n        final_df = pd.concat(all_data, ignore_index=True)  # Combine all pages\n        logger.info(f&quot;Fetched {len(final_df)} transactions across all pages.&quot;)\n        return final_df\n    return None\n\ndef validate_market_lookup(token_ids, market_lookup):\n    valid_token_ids = []\n    invalid_token_ids = []\n\n    for token_id in token_ids:\n        market_slug, outcome = find_market_info(token_id, market_lookup)\n        if market_slug and outcome:\n            valid_token_ids.append(token_id)\n        else:\n            invalid_token_ids.append(token_id)\n\n    logger.info(f&quot;Valid token IDs: {valid_token_ids}&quot;)\n    if invalid_token_ids:\n        logger.warning(f&quot;Invalid or missing market info for token IDs: {invalid_token_ids}&quot;)\n\n    return valid_token_ids\n\n\ndef sanitize_filename(filename):\n    &quot;&quot;&quot;\n    Sanitize the filename by removing or replacing invalid characters.\n    &quot;&quot;&quot;\n    # Replace invalid characters with an underscore\n    return re.sub(r&#039;[\\\\\/*?:&quot;|]', '_', filename)\n\ndef sanitize_directory(directory):\n    \"\"\"\n    Sanitize the directory name by removing or replacing invalid characters.\n    \"\"\"\n    # Replace invalid characters with an underscore\n    return re.sub(r'[\\\\\/*?:\"|]', '_', directory)\n\ndef extract_wallet_ids(leaderboard_url):\n    \"\"\"Scrape the Polymarket leaderboard to extract wallet IDs.\"\"\"\n    logging.info(f\"Fetching leaderboard page: {leaderboard_url}\")\n\n    response = requests.get(leaderboard_url)\n    if response.status_code != 200:\n        logging.error(f\"Failed to load page {leaderboard_url}, status code: {response.status_code}\")\n        return []\n\n    logging.debug(f\"Page loaded successfully, status code: {response.status_code}\")\n\n    soup = BeautifulSoup(response.content, 'html.parser')\n    logging.debug(\"Page content parsed with BeautifulSoup\")\n\n    wallet_ids = []\n\n    # Debug: Check if <a> tags are being found correctly\n    a_tags = soup.find_all('a', href=True)\n    logging.debug(f\"Found {len(a_tags)} <a> tags in the page.\")\n\n    for a_tag in a_tags:\n        href = a_tag['href']\n        logging.debug(f\"Processing href: {href}\")\n        if href.startswith('\/profile\/'):\n            wallet_id = href.split('\/')[-1]\n            wallet_ids.append(wallet_id)\n            logging.info(f\"Extracted wallet ID: {wallet_id}\")\n        else:\n            logging.debug(f\"Skipped href: {href}\")\n\n    return wallet_ids\ndef load_market_lookup(json_path):\n    \"\"\"Load market lookup data from a JSON file.\"\"\"\n    with open(json_path, 'r') as json_file:\n        return json.load(json_file)\n\n\n\n\ndef find_market_info(token_id, market_lookup):\n    \"\"\"Find market_slug and outcome based on tokenID.\"\"\"\n    token_id = str(token_id)  # Ensure token_id is a string\n    if not token_id or token_id == 'nan':\n        logger.warning(\"Token ID is NaN or empty. Skipping lookup.\")\n        return None, None\n\n    logger.debug(f\"Looking up market info for tokenID: {token_id}\")\n\n    for market in market_lookup.values():\n        for token in market['tokens']:\n            if str(token['token_id']) == token_id:\n                logger.debug(\n                    f\"Found market info for tokenID {token_id}: market_slug = {market['market_slug']}, outcome = {token['outcome']}\")\n                return market['market_slug'], token['outcome']\n\n    logger.warning(f\"No market info found for tokenID: {token_id}\")\n    return None, None\n\n\n\n\n\ndef fetch_data(url):\n    \"\"\"Fetch data from a given URL and return the JSON response.\"\"\"\n    response = requests.get(url)\n    return response.json()\n\n\ndef save_to_csv(filename, data, headers, output_dir):\n    \"\"\"Save data to a CSV file in the specified output directory.\"\"\"\n    filepath = os.path.join(output_dir, filename)\n    with open(filepath, 'w', newline='') as file:\n        writer = csv.DictWriter(file, fieldnames=headers)\n        writer.writeheader()\n        for entry in data:\n            writer.writerow(entry)\n    logger.info(f\"Saved data to {filepath}\")\n\n\n\ndef add_timestamps(erc1155_df, erc20_df):\n    \"\"\"\n    Rename timestamp columns and convert them from UNIX to datetime.\n    \"\"\"\n    # Rename the timestamp columns to avoid conflicts during merge\n    erc1155_df.rename(columns={'timeStamp': 'timeStamp_erc1155'}, inplace=True)\n    erc20_df.rename(columns={'timeStamp': 'timeStamp_erc20'}, inplace=True)\n\n    # Convert UNIX timestamps to datetime format\n    erc1155_df['timeStamp_erc1155'] = pd.to_numeric(erc1155_df['timeStamp_erc1155'], errors='coerce')\n    erc20_df['timeStamp_erc20'] = pd.to_numeric(erc20_df['timeStamp_erc20'], errors='coerce')\n\n    erc1155_df['timeStamp_erc1155'] = pd.to_datetime(erc1155_df['timeStamp_erc1155'], unit='s', errors='coerce')\n    erc20_df['timeStamp_erc20'] = pd.to_datetime(erc20_df['timeStamp_erc20'], unit='s', errors='coerce')\n\n    return erc1155_df, erc20_df\n\n\ndef enrich_erc1155_data(erc1155_df, market_lookup):\n    \"\"\"\n    Enrich the ERC-1155 DataFrame with market_slug and outcome based on market lookup.\n    \"\"\"\n\n    def get_market_info(token_id):\n        if pd.isna(token_id) or str(token_id) == 'nan':\n            return 'Unknown', 'Unknown'\n        for market in market_lookup.values():\n            for token in market['tokens']:\n                if str(token['token_id']) == str(token_id):\n                    return market['market_slug'], token['outcome']\n        return 'Unknown', 'Unknown'\n\n    erc1155_df['market_slug'], erc1155_df['outcome'] = zip(\n        *erc1155_df['tokenID'].apply(lambda x: get_market_info(x))\n    )\n\n    return erc1155_df\n\n\n\ndef get_transaction_details_by_hash(transaction_hash, api_key, output_dir='.\/data\/polymarket_trades\/'):\n    \"\"\"\n    Fetch the transaction details by hash from Polygonscan, parse the logs, and save the flattened data as a CSV.\n\n    Args:\n    - transaction_hash (str): The hash of the transaction.\n    - api_key (str): The Polygonscan API key.\n    - output_dir (str): The directory to save the CSV file.\n\n    Returns:\n    - None: Saves the transaction details to a CSV.\n    \"\"\"\n    # Ensure output directory exists\n    os.makedirs(output_dir, exist_ok=True)\n\n    # Construct the API URL for fetching transaction receipt details by hash\n    url = f\"https:\/\/api.polygonscan.com\/api?module=proxy&amp;action=eth_getTransactionReceipt&amp;txhash={transaction_hash}&amp;apikey={api_key}\"\n\n    logger.info(f\"Fetching transaction details for hash: {transaction_hash}\")\n    logger.debug(f\"Request URL: {url}\")\n\n    try:\n        # Fetch transaction details\n        response = requests.get(url)\n        logger.debug(f\"Polygonscan API response status: {response.status_code}\")\n\n        if response.status_code != 200:\n            logger.error(f\"Non-200 status code received: {response.status_code}\")\n            return None\n\n        # Parse the JSON response\n        data = response.json()\n        logger.debug(f\"Response JSON: {data}\")\n\n        # Check if the status is successful\n        if data.get('result') is None:\n            logger.error(f\"Error in API response: {data.get('message', 'Unknown error')}\")\n            return None\n\n        # Extract the logs\n        logs = data['result']['logs']\n        logs_df = pd.json_normalize(logs)\n\n        # Save the logs to a CSV file for easier review\n        csv_filename = os.path.join(output_dir, f\"transaction_logs_{transaction_hash}.csv\")\n        logs_df.to_csv(csv_filename, index=False)\n        logger.info(f\"Parsed logs saved to {csv_filename}\")\n\n        return logs_df\n\n    except Exception as e:\n        logger.error(f\"Exception occurred while fetching transaction details for hash {transaction_hash}: {e}\")\n        return None\ndef add_financial_columns(erc1155_df, erc20_df, wallet_id, market_lookup):\n    \"\"\"\n    Merge the ERC-1155 and ERC-20 dataframes, calculate financial columns,\n    including whether a trade was won or lost, and fetch the latest price for each contract and tokenID.\n    \"\"\"\n    # Merge the two dataframes on the 'hash' column\n    merged_df = pd.merge(erc1155_df, erc20_df, how='outer', on='hash', suffixes=('_erc1155', '_erc20'))\n\n    # Convert wallet ID and columns to lowercase for case-insensitive comparison\n    wallet_id = wallet_id.lower()\n    merged_df['to_erc1155'] = merged_df['to_erc1155'].astype(str).str.lower()\n    merged_df['from_erc1155'] = merged_df['from_erc1155'].astype(str).str.lower()\n\n    # Remove rows where 'tokenID' is NaN or 'nan'\n    merged_df['tokenID'] = merged_df['tokenID'].astype(str)\n    merged_df = merged_df[~merged_df['tokenID'].isnull() &amp; (merged_df['tokenID'] != 'nan')]\n\n\n    # Set transaction type based on wallet address\n    merged_df['transaction_type'] = 'other'\n    merged_df.loc[merged_df['to_erc1155'] == wallet_id, 'transaction_type'] = 'buy'\n    merged_df.loc[merged_df['from_erc1155'] == wallet_id, 'transaction_type'] = 'sell'\n\n    # Calculate the purchase price per token and total dollar value\n    if 'value' in merged_df.columns and 'tokenValue' in merged_df.columns:\n        merged_df['price_paid_per_token'] = merged_df['value'].astype(float) \/ merged_df['tokenValue'].astype(float)\n        merged_df['total_purchase_value'] = merged_df['value'].astype(float) \/ 10**6  # USDC has 6 decimal places\n        merged_df['shares'] = merged_df['total_purchase_value'] \/ merged_df['price_paid_per_token']\n    else:\n        logger.error(\"The necessary columns for calculating purchase price are missing.\")\n        return merged_df\n\n    # Create the 'lost' and 'won' columns\n    merged_df['lost'] = (\n        (merged_df['to_erc1155'] == '0x0000000000000000000000000000000000000000') &amp;\n        (merged_df['transaction_type'] == 'sell') &amp;\n        (merged_df['price_paid_per_token'].isna() | (merged_df['price_paid_per_token'] == 0))\n    ).astype(int)\n\n    merged_df['won'] = (\n        (merged_df['transaction_type'] == 'sell') &amp;\n        (merged_df['price_paid_per_token'] == 1)\n    ).astype(int)\n\n    merged_df.loc[merged_df['lost'] == 1, 'shares'] = 0\n    merged_df.loc[merged_df['lost'] == 1, 'total_purchase_value'] = 0\n\n    # Fetch live prices and calculate profit\/loss (pl)\n    merged_df['tokenID'] = merged_df['tokenID'].astype(str)\n    merged_df = update_latest_prices(merged_df, market_lookup)\n\n    return merged_df\n\ndef plot_profit_loss_by_trade(df, user_info):\n    \"\"\"\n    Create a bar plot to visualize aggregated Profit\/Loss (PL) by trade, with values rounded to two decimal places and formatted as currency.\n\n    Args:\n        df (DataFrame): DataFrame containing trade data, including 'market_slug', 'outcome', and 'pl'.\n        user_info (dict): Dictionary containing user information, such as username, wallet address, and other relevant details.\n    \"\"\"\n    if 'pl' not in df.columns or df['pl'].isnull().all():\n        logger.warning(\"No PL data available for plotting. Skipping plot.\")\n        return\n\n    username = user_info.get(\"username\", \"Unknown User\")\n    wallet_id = user_info.get(\"wallet_address\", \"N\/A\")\n    positions_value = user_info.get(\"positions_value\", \"N\/A\")\n    profit_loss = user_info.get(\"profit_loss\", \"N\/A\")\n    volume_traded = user_info.get(\"volume_traded\", \"N\/A\")\n    markets_traded = user_info.get(\"markets_traded\", \"N\/A\")\n\n    # Combine market_slug and outcome to create a trade identifier\n    df['trade'] = df['market_slug'] + ' (' + df['outcome'] + ')'\n\n    # Aggregate the Profit\/Loss (pl) for each unique trade\n    aggregated_df = df.groupby('trade', as_index=False).agg({'pl': 'sum'})\n\n    # Round PL values to two decimal places\n    aggregated_df['pl'] = aggregated_df['pl'].round(2)\n\n    # Format the PL values with a dollar sign for display\n    aggregated_df['pl_display'] = aggregated_df['pl'].apply(lambda x: f\"${x:,.2f}\")\n\n    # Define a color mapping based on Profit\/Loss sign\n    aggregated_df['color'] = aggregated_df['pl'].apply(lambda x: 'green' if x &gt;= 0 else 'red')\n\n    # Create the plot without using the color axis\n    fig = px.bar(\n        aggregated_df,\n        x='trade',\n        y='pl',\n        title='',\n        labels={'pl': 'Profit\/Loss ($)', 'trade': 'Trade (Market Slug \/ Outcome)'},\n        text='pl_display',\n        color='color',  # Use the color column\n        color_discrete_map={'green': 'green', 'red': 'red'},\n    )\n\n    # Remove the legend if you don't want it\n    fig.update_layout(showlegend=False)\n\n    # Rotate x-axis labels for better readability and set the main title\n    fig.update_layout(\n        title={\n            'text': 'Aggregated Profit\/Loss by Trade',\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 24}\n        },\n        xaxis_tickangle=-45,\n        margin=dict(t=150, l=50, r=50, b=100)\n    )\n\n    # Prepare the subtitle text with user information\n    subtitle_text = (\n        f\"Username: {username} | Positions Value: {positions_value} | \"\n        f\"Profit\/Loss: {profit_loss} | Volume Traded: {volume_traded} | \"\n        f\"Markets Traded: {markets_traded} | Wallet ID: {wallet_id}\"\n    )\n\n    # Add the subtitle as an annotation\n    fig.add_annotation(\n        text=subtitle_text,\n        xref=\"paper\",\n        yref=\"paper\",\n        x=0.5,\n        y=1.02,\n        xanchor='center',\n        yanchor='top',\n        showarrow=False,\n        font=dict(size=14)\n    )\n\n    # Save the plot\n    plot_dir = \".\/plots\/user_trades\"\n    os.makedirs(plot_dir, exist_ok=True)\n    sanitized_username = sanitize_filename(username)\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_aggregated_profit_loss_by_trade.html\")\n    fig.write_html(plot_file)\n\n    logger.info(f\"Aggregated Profit\/Loss by trade plot saved to {plot_file}\")\n\n\n\ndef plot_shares_over_time(df, user_info):\n    \"\"\"\n    Create a line plot to visualize the cumulative number of shares for each token over time.\n    Buy orders add to the position, and sell orders subtract from it.\n\n    Args:\n        df (DataFrame): DataFrame containing trade data, including 'timeStamp_erc1155', 'shares', 'market_slug', 'outcome', and 'transaction_type' ('buy' or 'sell').\n        user_info (dict): Dictionary containing user information, such as username, wallet address, and other relevant details.\n    \"\"\"\n    if 'shares' not in df.columns or df['shares'].isnull().all():\n        logger.warning(\"No 'shares' data available for plotting. Skipping plot.\")\n        return\n\n    username = user_info.get(\"username\", \"Unknown User\")\n\n    # Ensure 'timeStamp_erc1155' is a datetime type, just in case it needs to be converted\n    if df['timeStamp_erc1155'].dtype != 'datetime64[ns]':\n        df['timeStamp_erc1155'] = pd.to_datetime(df['timeStamp_erc1155'], errors='coerce')\n\n    # Drop rows with NaN values in 'timeStamp_erc1155', 'shares', 'market_slug', 'outcome', or 'transaction_type'\n    df = df.dropna(subset=['timeStamp_erc1155', 'shares', 'market_slug', 'outcome', 'transaction_type'])\n\n    # Sort the dataframe by time to ensure the line chart shows the data in chronological order\n    df = df.sort_values(by='timeStamp_erc1155')\n\n    # Combine 'market_slug' and 'outcome' to create a unique label for each token\n    df['token_label'] = df['market_slug'] + \" - \" + df['outcome']\n\n    # Create a column for 'position_change' which adds shares for buys and subtracts shares for sells based on 'transaction_type'\n    df['position_change'] = df.apply(lambda row: row['shares'] if row['transaction_type'] == 'buy' else -row['shares'], axis=1)\n\n    # Group by 'token_label' and calculate the cumulative position\n    df['cumulative_position'] = df.groupby('token_label')['position_change'].cumsum()\n\n    # Forward fill the cumulative position to maintain it between trades\n    df['cumulative_position'] = df.groupby('token_label')['cumulative_position'].ffill()\n\n    # Create the line plot, grouping by 'token_label' for separate lines per token ID\n    fig = px.line(\n        df,\n        x='timeStamp_erc1155',\n        y='cumulative_position',\n        color='token_label',  # This ensures each token ID (market_slug + outcome) gets its own line\n        title=f'Cumulative Shares Over Time for {username}',\n        labels={'timeStamp_erc1155': 'Trade Time', 'cumulative_position': 'Cumulative Position', 'token_label': 'Token (Market Slug - Outcome)'},\n        line_shape='linear'\n    )\n\n    # Update layout for better aesthetics\n    fig.update_layout(\n        title={\n            'text': f\"Cumulative Number of Shares Over Time for {username}\",\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 20}\n        },\n        margin=dict(t=60),\n        xaxis_title=\"Trade Time\",\n        yaxis_title=\"Cumulative Number of Shares\",\n        legend_title=\"Token (Market Slug - Outcome)\"\n    )\n\n    # Save the plot\n    plot_dir = \".\/plots\/user_trades\"\n    os.makedirs(plot_dir, exist_ok=True)\n    sanitized_username = sanitize_filename(username)\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_shares_over_time.html\")\n    fig.write_html(plot_file)\n\n    logger.info(f\"Cumulative shares over time plot saved to {plot_file}\")\n\n\ndef plot_user_trades(df, user_info):\n    \"\"\"Plot user trades and save plots, adjusting for trades that were lost.\"\"\"\n    username = user_info[\"username\"]\n    wallet_id = user_info[\"wallet_address\"]\n\n    # Sanitize only the filename, not the directory\n    sanitized_username = sanitize_filename(username)\n\n    info_text = (\n        f\"Username: {username} | Positions Value: {user_info['positions_value']} | \"\n        f\"Profit\/Loss: {user_info['profit_loss']} | Volume Traded: {user_info['volume_traded']} | \"\n        f\"Markets Traded: {user_info['markets_traded']} | Wallet ID: {wallet_id}\"\n    )\n\n    # Ensure the directory exists\n    os.makedirs(\".\/plots\/user_trades\", exist_ok=True)\n    plot_dir = \".\/plots\/user_trades\"\n\n    # Flag loss trades where to_erc1155 is zero address, transaction_type is sell, and price_paid_per_token is NaN\n    df['is_loss'] = df.apply(\n        lambda row: (row['to_erc1155'] == '0x0000000000000000000000000000000000000000')\n                    and (row['transaction_type'] == 'sell')\n                    and pd.isna(row['price_paid_per_token']), axis=1)\n\n    # Set shares and total purchase value to zero for loss trades\n    df.loc[df['is_loss'], 'shares'] = 0\n    df.loc[df['is_loss'], 'total_purchase_value'] = 0\n\n    ### Modify for Total Purchase Value by Market (Current holdings)\n    df['total_purchase_value_adjusted'] = df.apply(\n        lambda row: row['total_purchase_value'] if row['transaction_type'] == 'buy' else -row['total_purchase_value'],\n        axis=1\n    )\n\n    grouped_df_value = df.groupby(['market_slug']).agg({\n        'total_purchase_value_adjusted': 'sum',\n        'shares': 'sum',\n    }).reset_index()\n\n    # Calculate the weighted average price_paid_per_token\n    grouped_df_value['weighted_price_paid_per_token'] = (\n        grouped_df_value['total_purchase_value_adjusted'] \/ grouped_df_value['shares']\n    )\n\n    # Sort by total_purchase_value in descending order (ignoring outcome)\n    grouped_df_value = grouped_df_value.sort_values(by='total_purchase_value_adjusted', ascending=False)\n\n    # Format the label for the bars (removing outcome)\n    grouped_df_value['bar_label'] = (\n        \"Avg Price: $\" + grouped_df_value['weighted_price_paid_per_token'].round(2).astype(str)\n    )\n\n    fig = px.bar(\n        grouped_df_value,\n        x='market_slug',\n        y='total_purchase_value_adjusted',\n        barmode='group',\n        title=f\"Current Total Purchase Value by Market for {username}\",\n        labels={'total_purchase_value_adjusted': 'Current Total Purchase Value', 'market_slug': 'Market'},\n        text=grouped_df_value['bar_label'],\n        hover_data={'weighted_price_paid_per_token': ':.2f'},\n    )\n\n    fig.update_layout(\n        title={\n            'text': f\"Current Total Purchase Value by Market for {username}\",\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 20}\n        },\n        margin=dict(t=60),\n        showlegend=False  # Remove the legend as you requested\n    )\n\n    fig.add_annotation(\n        text=info_text,\n        xref=\"paper\", yref=\"paper\", showarrow=False, x=0.5, y=1.05, font=dict(size=12)\n    )\n\n    # Save the bar plot as an HTML file\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_current_market_purchase_value.html\")\n    fig.write_html(plot_file)\n    logger.info(f\"Current market purchase value plot saved to {plot_file}\")\n\n    ### Modify for Trade Quantity by Market (Current holdings)\n    df['shares_adjusted'] = df.apply(\n        lambda row: row['shares'] if row['transaction_type'] == 'buy' else -row['shares'], axis=1)\n\n    grouped_df_quantity = df.groupby(['market_slug']).agg({\n        'shares_adjusted': 'sum',\n        'total_purchase_value': 'sum',\n    }).reset_index()\n\n    # Calculate the weighted average price_paid_per_token\n    grouped_df_quantity['weighted_price_paid_per_token'] = (\n        grouped_df_quantity['total_purchase_value'] \/ grouped_df_quantity['shares_adjusted']\n    )\n\n    grouped_df_quantity = grouped_df_quantity.sort_values(by='shares_adjusted', ascending=False)\n\n    grouped_df_quantity['bar_label'] = (\n        \"Quantity: \" + grouped_df_quantity['shares_adjusted'].round().astype(int).astype(str) + \"<br>\" +\n        \"Avg Price: $\" + grouped_df_quantity['weighted_price_paid_per_token'].round(2).astype(str)\n    )\n\n    fig = px.bar(\n        grouped_df_quantity,\n        x='market_slug',\n        y='shares_adjusted',\n        barmode='group',\n        title=f\"Current Trade Quantity by Market for {username}\",\n        labels={'shares_adjusted': 'Current Trade Quantity', 'market_slug': 'Market'},\n        text=grouped_df_quantity['bar_label'],\n    )\n\n    fig.update_layout(\n        title={\n            'text': f\"Current Trade Quantity by Market for {username}\",\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 20}\n        },\n        margin=dict(t=60),\n        showlegend=False  # Remove the legend as you requested\n    )\n\n    fig.add_annotation(\n        text=info_text,\n        xref=\"paper\", yref=\"paper\", showarrow=False, x=0.5, y=1.05, font=dict(size=12)\n    )\n\n    # Save the trade quantity plot as an HTML file\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_current_market_trade_quantity.html\")\n    fig.write_html(plot_file)\n    logger.info(f\"Current market trade quantity plot saved to {plot_file}\")\n\n    ### Modify for Total Purchase Value Timeline\n    df['total_purchase_value_timeline_adjusted'] = df.apply(\n        lambda row: row['total_purchase_value'] if row['transaction_type'] == 'buy' else -row['total_purchase_value'],\n        axis=1\n    )\n\n    # Combine 'market_slug' and 'outcome' into a unique label\n    df['market_outcome_label'] = df['market_slug'] + ' (' + df['outcome'] + ')'\n\n    # Create the scatter plot, now coloring by 'market_outcome_label'\n    fig = px.scatter(\n        df,\n        x='timeStamp_erc1155',\n        y='total_purchase_value_timeline_adjusted',\n        color='market_outcome_label',  # Use the combined label for market and outcome\n        title=f\"Total Purchase Value Timeline for {username}\",\n        labels={\n            'total_purchase_value_timeline_adjusted': 'Total Purchase Value',\n            'timeStamp_erc1155': 'Transaction Time',\n            'market_outcome_label': 'Market\/Outcome'\n        },\n        hover_data=['market_slug', 'price_paid_per_token', 'outcome', 'hash'],\n    )\n\n    fig.update_layout(\n        title={\n            'text': f\"Total Purchase Value Timeline for {username}\",\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 20}\n        },\n        margin=dict(t=60)\n    )\n\n    fig.add_annotation(\n        text=info_text,\n        xref=\"paper\", yref=\"paper\", showarrow=False, x=0.5, y=1.05, font=dict(size=12)\n    )\n\n    # Save the updated plot\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_total_purchase_value_timeline_adjusted.html\")\n    fig.write_html(plot_file)\n    logger.info(f\"Total purchase value timeline plot saved to {plot_file}\")\n\ndef plot_total_purchase_value(df, user_info):\n    \"\"\"Create and save a scatter plot for total purchase value, accounting for buy and sell transactions.\"\"\"\n    # Ensure the directory exists\n    os.makedirs(\".\/plots\/user_trades\", exist_ok=True)\n    plot_dir = \".\/plots\/user_trades\"\n\n    username = user_info[\"username\"]\n    wallet_id = user_info[\"wallet_address\"]\n\n    # Sanitize only the filename, not the directory\n    sanitized_username = sanitize_filename(username)\n\n    info_text = (\n        f\"Username: {username} | Positions Value: {user_info['positions_value']} | \"\n        f\"Profit\/Loss: {user_info['profit_loss']} | Volume Traded: {user_info['volume_traded']} | \"\n        f\"Markets Traded: {user_info['markets_traded']} | Wallet ID: {wallet_id}\"\n    )\n\n    # Flag loss trades where to_erc1155 is zero address, transaction_type is sell, and price_paid_per_token is NaN\n    df['is_loss'] = df.apply(\n        lambda row: (row['to_erc1155'] == '0x0000000000000000000000000000000000000000')\n                    and (row['transaction_type'] == 'sell')\n                    and pd.isna(row['price_paid_per_token']), axis=1)\n\n    # Set shares and total purchase value to zero for loss trades\n    df.loc[df['is_loss'], 'shares'] = 0\n    df.loc[df['is_loss'], 'total_purchase_value'] = 0\n\n    # Adjust the total purchase value based on the transaction type\n    df['total_purchase_value_adjusted'] = df.apply(\n        lambda row: row['total_purchase_value'] if row['transaction_type'] == 'buy' else -row['total_purchase_value'],\n        axis=1\n    )\n\n    # Create the scatter plot for total purchase value over time\n    fig = px.scatter(\n        df,\n        x='timeStamp_erc1155',  # Assuming this is the correct timestamp field\n        y='total_purchase_value_adjusted',  # Adjusted values for buys and sells\n        color='market_slug',  # Use market_slug with outcome as the color\n        title=f\"Current Purchase Value Timeline for {username}\",  # Update title to reflect \"current\"\n        labels={'total_purchase_value_adjusted': 'Adjusted Purchase Value ($)', 'timeStamp_erc1155': 'Transaction Time'},\n        hover_data=['market_slug', 'price_paid_per_token', 'outcome', 'hash'],\n    )\n\n    # Adjust title positioning and font size\n    fig.update_layout(\n        title={\n            'text': f\"Current Purchase Value Timeline for {username}\",  # Update to \"Current\"\n            'y': 0.95,\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top',\n            'font': {'size': 20}\n        },\n        margin=dict(t=60)\n    )\n\n    fig.add_annotation(\n        text=info_text,\n        xref=\"paper\", yref=\"paper\", showarrow=False, x=0.5, y=1.05, font=dict(size=12)\n    )\n\n    # Save the scatter plot as an HTML file\n    plot_file = os.path.join(plot_dir, f\"{sanitized_username}_current_purchase_value_timeline.html\")\n    fig.write_html(plot_file)\n    logger.info(f\"Current purchase value timeline plot saved to {plot_file}\")\n\ndef create_and_save_pie_chart(df, user_info):\n    \"\"\"Create and save a pie chart for user's current holdings.\"\"\"\n    # Ensure the directory exists\n    os.makedirs(\".\/plots\/user_trades\", exist_ok=True)\n    plot_dir = \".\/plots\/user_trades\"\n    username = user_info[\"username\"]\n    wallet_id = user_info[\"wallet_address\"]\n\n    sanitized_username = sanitize_filename(username)\n\n    info_text = (\n        f\"Username: {username} | Positions Value: {user_info['positions_value']} | \"\n        f\"Profit\/Loss: {user_info['profit_loss']} | Volume Traded: {user_info['volume_traded']} | \"\n        f\"Markets Traded: {user_info['markets_traded']} | Wallet ID: {wallet_id}\"\n    )\n\n    # Flag loss trades where to_erc1155 is zero address, transaction_type is sell, and price_paid_per_token is NaN\n    df['is_loss'] = df.apply(\n        lambda row: (row['to_erc1155'] == '0x0000000000000000000000000000000000000000')\n                    and (row['transaction_type'] == 'sell')\n                    and pd.isna(row['price_paid_per_token']), axis=1)\n\n    # Set shares and total purchase value to zero for loss trades\n    df.loc[df['is_loss'], 'shares'] = 0\n\n    df['shares_adjusted'] = df.apply(\n        lambda row: row['shares'] if row['transaction_type'] == 'buy' else -row['shares'], axis=1)\n\n    holdings = df.groupby('market_slug').agg({'shares_adjusted': 'sum'}).reset_index()\n\n    holdings = holdings.sort_values('shares_adjusted', ascending=False)\n    threshold = 0.02\n    large_slices = holdings[holdings['shares_adjusted'] &gt; holdings['shares_adjusted'].sum() * threshold]\n    small_slices = holdings[holdings['shares_adjusted']  0:\n                all_data.extend(data['result'])\n                page += 1\n            else:\n                break  # Stop if no more data is returned\n\n        return pd.DataFrame(all_data)\n\n    # Fetch ERC-20 transactions with pagination\n    erc20_url = (f\"https:\/\/api.polygonscan.com\/api\"\n                 f\"?module=account\"\n                 f\"&amp;action=tokentx\"\n                 f\"&amp;address={wallet_address}\"\n                 f\"&amp;startblock=0\"\n                 f\"&amp;endblock=99999999\"\n                 f\"&amp;sort=desc\"\n                 f\"&amp;apikey={api_key}\")\n\n    erc20_df = fetch_paginated_data(erc20_url)\n\n    # Fetch ERC-1155 transactions with pagination\n    erc1155_url = (f\"https:\/\/api.polygonscan.com\/api\"\n                   f\"?module=account\"\n                   f\"&amp;action=token1155tx\"\n                   f\"&amp;address={wallet_address}\"\n                   f\"&amp;startblock=0\"\n                   f\"&amp;endblock=99999999\"\n                   f\"&amp;sort=desc\"\n                   f\"&amp;apikey={api_key}\")\n\n    erc1155_df = fetch_paginated_data(erc1155_url)\n\n    if not erc20_df.empty and not erc1155_df.empty:\n        return erc20_df, erc1155_df\n    else:\n        return None, None\n\n\ndef fetch_wallet_addresses(skip_leaderboard, top_volume, top_profit):\n    \"\"\"\n    Fetch wallet addresses based on leaderboard data or manual input.\n\n    Args:\n        skip_leaderboard (bool): Whether to skip leaderboard fetching.\n        top_volume (bool): Fetch top volume users.\n        top_profit (bool): Fetch top profit users.\n\n    Returns:\n        list: A list of wallet addresses to process.\n    \"\"\"\n    # Manually specified wallet addresses\n    manual_wallet_ids = [\n        '0x76527252D7FEd00dC4D08d794aFa1cCC36069C2a',\n        # Add more wallet IDs as needed\n    ]\n\n    if not skip_leaderboard:\n        leaderboard_wallet_ids = call_scrape_wallet_ids(top_volume=top_volume, top_profit=top_profit)\n        wallet_addresses = list(set(manual_wallet_ids + leaderboard_wallet_ids))  # Remove duplicates\n    else:\n        wallet_addresses = manual_wallet_ids\n\n    return wallet_addresses\n\ndef main(wallet_addresses=None, skip_leaderboard=False, top_volume=False, top_profit=False, plot=True, latest_price_mode=False):\n\n    \"\"\"\n    Main function to process wallet data and generate plots.\n\n    Args:\n        wallet_addresses (list): A list of wallet addresses to process (if provided).\n        skip_leaderboard (bool): Whether to skip fetching leaderboard data.\n        top_volume (bool): Whether to fetch top volume users.\n        top_profit (bool): Whether to fetch top profit users.\n        plot (bool): Whether to generate plots for the user data.\n        latest_price_mode (bool): If True, only retrieve the latest prices, no plotting.\n    \"\"\"\n    # Load environment variables\n    load_dotenv(\"keys.env\")\n    api_key = os.getenv('POLYGONSCAN_API_KEY')\n\n    if not wallet_addresses:\n        # Fetch wallet addresses if not provided\n        wallet_addresses = fetch_wallet_addresses(skip_leaderboard, top_volume, top_profit)\n\n    # Process wallet data and optionally generate plots\n    process_and_plot_user_data(wallet_addresses, api_key, plot=plot, latest_price_mode=latest_price_mode)\n\n\nif __name__ == \"__main__\":\n    # Use argparse to accept command-line arguments\n    parser = argparse.ArgumentParser(description='Process wallet data for specific wallet addresses.')\n\n    parser.add_argument(\n        '--wallets',\n        nargs='+',  # This will accept multiple wallet IDs\n        help='List of wallet addresses to process.'\n    )\n    parser.add_argument('--skip-leaderboard', action='store_true', help='Skip leaderboard fetching.')\n    parser.add_argument('--top-volume', action='store_true', help='Fetch top volume users.')\n    parser.add_argument('--top-profit', action='store_true', help='Fetch top profit users.')\n    parser.add_argument('--no-plot', action='store_true', help='Disable plot generation.')\n    parser.add_argument('--latest-price-mode', action='store_true',\n                        help='Only retrieve the latest prices, no plotting.')\n\n    args = parser.parse_args()\n\n    # Call the main function with the parsed arguments\n    main(\n        wallet_addresses=args.wallets,\n        skip_leaderboard=args.skip_leaderboard,\n        top_volume=args.top_volume,\n        top_profit=args.top_profit,\n        plot=not args.no_plot,\n        latest_price_mode=args.latest_price_mode\n    )<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">plot_arb.py<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This code will take all of your strategies and then plot the arb into a nice HTML file for review. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11906,\"width\":\"614px\",\"height\":\"auto\",\"sizeSlug\":\"large\",\"linkDestination\":\"media\"} -->\n<figure class=\"wp-block-image size-large is-resized\"><a href=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-35.png\"><img decoding=\"async\" width=\"523\" height=\"1024\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-35-523x1024.png\" alt=\"\" class=\"wp-image-11906 lazyload\" style=\"--smush-placeholder-width: 523px; --smush-placeholder-aspect-ratio: 523\/1024;width:614px;height:auto\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-35-523x1024.png 523w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-35-153x300.png 153w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-35-768x1504.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-35-784x1536.png 784w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-35-1046x2048.png 1046w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/newplot-35.png 1685w\" data-sizes=\"(max-width: 523px) 100vw, 523px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" \/><\/a><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport subprocess\nimport pandas as pd\nimport plotly.graph_objects as go\nfrom plotly.subplots import make_subplots\nimport time\nimport pytz\nimport logging\nfrom strategies import trades\nimport json\n# Setup logging\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n\nplot_dir = \".\/plots\/\"\nos.makedirs(plot_dir, exist_ok=True)\n\n\n\ndef load_market_lookup():\n    \"\"\"\n    Loads the market lookup JSON and maps slugs to token IDs based on outcomes.\n    \"\"\"\n    with open('.\/data\/market_lookup.json', 'r') as f:\n        market_lookup = json.load(f)\n\n    slug_to_token_id = {}\n    for market in market_lookup.values():\n        slug = market['market_slug']\n        slug_to_token_id[slug] = {token['outcome']: token['token_id'] for token in market['tokens']}\n\n    return slug_to_token_id\n\ndef run_get_trade_slugs_to_parquet(token_id, market_slug, outcome):\n    \"\"\"\n    Runs the get_trade_slugs_to_parquet.py script with the specified arguments to fetch and save timeseries data.\n    \"\"\"\n    script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'get_trade_slugs_to_parquet.py')\n    try:\n        result = subprocess.run([\"python3\", script_path, token_id, market_slug, outcome], capture_output=True, text=True)\n        if result.returncode != 0:\n            logging.error(f\"Error running get_trade_slugs_to_parquet.py: {result.stderr}\")\n            raise RuntimeError(\"Failed to run get_trade_slugs_to_parquet.py\")\n        logging.info(\"get_trade_slugs_to_parquet.py ran successfully.\")\n    except Exception as e:\n        logging.exception(\"Failed to run the script.\")\n\ndef process_and_fetch_data(trade_slug, outcome):\n    \"\"\"\n    Fetches the token_id for the given trade_slug and outcome and runs the get_trade_slugs_to_parquet function.\n    \"\"\"\n    market_lookup = load_market_lookup()  # Load the market lookup from JSON\n\n    if trade_slug in market_lookup and outcome in market_lookup[trade_slug]:\n        token_id = market_lookup[trade_slug][outcome]\n        run_get_trade_slugs_to_parquet(token_id, trade_slug, outcome)\n    else:\n        logging.error(f\"Slug '{trade_slug}' or outcome '{outcome}' not found in the lookup.\")\n\n\n\ndef plot_parquet(file_path):\n    \"\"\"\n    Plots the data from a Parquet file and saves the plot as an HTML file.\n    \"\"\"\n    try:\n        df = pd.read_parquet(file_path)\n        if 'timestamp' in df.columns and 'price' in df.columns:\n            fig = go.Figure()\n            fig.add_trace(go.Scatter(x=df['timestamp'], y=df['price'], mode='lines',\n                                     name=os.path.basename(file_path).replace('.parquet', '')))\n            output_file = os.path.join(plot_dir, os.path.basename(file_path).replace('.parquet', '.html'))\n            fig.write_html(output_file)\n            logging.info(f\"Plot saved to {output_file}\")\n        else:\n            logging.warning(f\"Data in {file_path} does not contain expected columns 'timestamp' and 'price'\")\n    except Exception as e:\n        logging.exception(f\"Failed to plot data from Parquet file {file_path}\")\n\n\ndef plot_trade_sides(trade_name, subtitle, side_a_trades=None, side_b_trades=None, positions=None, method=\"balanced\", timezone='America\/Phoenix', plot_last_x_days=14):\n    \"\"\"\n    Plots cumulative data for two sides of a trade, the arbitrage percentage, and individual slug data.\n    Includes a subtitle for the chart. Only the last plot_last_x_days days of data will be plotted.\n\n    :param trade_name: The name of the trade.\n    :param subtitle: The subtitle or hypothesis to be displayed below the title.\n    :param side_a_trades: List of tuples for side A trades, where each tuple is (slug, 'Yes' or 'No').\n    :param side_b_trades: List of tuples for side B trades, where each tuple is (slug, 'Yes' or 'No').\n    :param positions: List of positions (slug, 'No') for the 'all_no' method.\n    :param method: The method to use for arbitrage calculation (\"balanced\" or \"all_no\").\n    :param timezone: The desired timezone for displaying timestamps. Default is 'America\/Phoenix'.\n    :param plot_last_x_days: The number of days of data to display in the plot. Default is 14.\n    \"\"\"\n\n    def convert_timezone(df, timezone):\n        df['timestamp'] = pd.to_datetime(df['timestamp'], utc=True)\n        df['timestamp'] = df['timestamp'].dt.tz_convert(timezone)\n        return df\n\n    def truncate_data_for_plotting(df, plot_last_x_days):\n        if not df.empty:\n            last_timestamp = df['timestamp'].max()\n            cutoff_timestamp = last_timestamp - pd.Timedelta(days=plot_last_x_days)\n            df = df[df['timestamp'] &gt;= cutoff_timestamp]\n        return df\n\n    def aggregate_side(trades):\n        combined_df = pd.DataFrame()\n        missing_files = []\n        missing_columns = []\n\n        for slug, outcome in trades:\n            file_path = f\".\/data\/historical\/{slug}_{outcome}.parquet\"\n            if os.path.exists(file_path):\n                df = pd.read_parquet(file_path)\n                if 'timestamp' in df.columns and 'price' in df.columns:\n                    df = convert_timezone(df, timezone)\n                    df = df.drop_duplicates(subset='timestamp')\n                    df = df.set_index('timestamp').resample('min').ffill().reset_index()\n\n                    if combined_df.empty:\n                        combined_df = df[['timestamp', 'price']].copy()\n                    else:\n                        combined_df = pd.merge(combined_df, df[['timestamp', 'price']], on='timestamp', how='outer')\n                        combined_df['price'] = combined_df['price_x'].fillna(0) + combined_df['price_y'].fillna(0)\n                        combined_df = combined_df[['timestamp', 'price']]\n                else:\n                    missing_columns.append(file_path)\n                    print(f\"Data in {file_path} does not contain expected columns 'timestamp' and 'price'\")\n                    return None, missing_files, missing_columns\n            else:\n                missing_files.append(file_path)\n                print(f\"File {file_path} does not exist.\")\n                return None, missing_files, missing_columns\n\n        return truncate_data_for_plotting(combined_df, plot_last_x_days), missing_files, missing_columns\n\n    def aggregate_positions(positions):\n        combined_df = pd.DataFrame()\n        missing_files = []\n        missing_columns = []\n\n        for slug, outcome in positions:\n            file_path = f\".\/data\/historical\/{slug}_{outcome}.parquet\"\n            if os.path.exists(file_path):\n                df = pd.read_parquet(file_path)\n                if 'timestamp' in df.columns and 'price' in df.columns:\n                    df = convert_timezone(df, timezone)\n                    df = df.drop_duplicates(subset='timestamp')\n                    df = df.set_index('timestamp').resample('min').ffill().reset_index()\n                    df = df.rename(columns={'price': f'price_{slug}'})\n\n                    if combined_df.empty:\n                        combined_df = df\n                    else:\n                        combined_df = pd.merge(combined_df, df, on='timestamp', how='outer')\n\n                else:\n                    missing_columns.append(file_path)\n                    print(f\"Data in {file_path} does not contain expected columns 'timestamp' and 'price'\")\n                    return None, missing_files, missing_columns\n            else:\n                missing_files.append(file_path)\n                print(f\"File {file_path} does not exist.\")\n                return None, missing_files, missing_columns\n\n        combined_df = combined_df.dropna()\n        return truncate_data_for_plotting(combined_df, plot_last_x_days), missing_files, missing_columns\n\n    def calculate_arbitrage_balanced(combined_df):\n        combined_df['total_cost'] = combined_df['price_a'] + combined_df['price_b']\n        combined_df['arb_percentage'] = (1 - combined_df['total_cost']) * 100\n\n    def calculate_arbitrage_all_no(positions_df):\n        if positions_df.empty:\n            return pd.DataFrame(), \"No data available\"\n\n        price_columns = positions_df.columns.drop('timestamp')\n        no_prices_df = 1 - positions_df[price_columns]\n        arb_percentages = []\n\n        for _, row in positions_df.iterrows():\n            min_no_price = no_prices_df.loc[row.name].min()\n            total_winnings = no_prices_df.loc[row.name].sum() - min_no_price\n            arb_percentage = (total_winnings - (1 - min_no_price)) * 100\n            arb_percentages.append(arb_percentage)\n\n        positions_df['arb_percentage'] = arb_percentages\n        return positions_df, None\n\n    def calculate_bollinger_bands(series, window=2880, num_std_dev=1):\n        rolling_mean = series.rolling(window=window).mean()\n        rolling_std = series.rolling(window=window).std()\n        upper_band = rolling_mean + (rolling_std * num_std_dev)\n        lower_band = rolling_mean - (rolling_std * num_std_dev)\n        return rolling_mean, upper_band, lower_band\n\n    def plot_individual_slugs(trades, fig, start_row, color):\n        for i, (slug, outcome) in enumerate(trades):\n            file_path = f\".\/data\/historical\/{slug}_{outcome}.parquet\"\n            print(f\"Processing individual slug: {file_path}\")\n            if os.path.exists(file_path):\n                df = pd.read_parquet(file_path)\n                if 'timestamp' in df.columns and 'price' in df.columns:\n                    df = df.drop_duplicates(subset='timestamp')\n\n                    # Convert the timezone for the slug data\n                    df = convert_timezone(df, timezone)\n\n                    df = df.set_index('timestamp').resample('min').ffill().reset_index()\n\n                    df = truncate_data_for_plotting(df, plot_last_x_days)\n\n                    df['rolling_mean'], df['bollinger_upper'], df['bollinger_lower'] = calculate_bollinger_bands(\n                        df['price'])\n\n                    fig.add_trace(go.Scatter(\n                        x=df['timestamp'],\n                        y=df['price'],\n                        mode='lines',\n                        name=f\"{slug} ({outcome})\",\n                        line=dict(width=1, color=color),\n                        showlegend=False\n                    ), row=start_row + i, col=1)\n\n                    fig.add_trace(go.Scatter(\n                        x=df['timestamp'],\n                        y=df['bollinger_upper'],\n                        mode='lines',\n                        name=f\"{slug} ({outcome}) Upper BB\",\n                        line=dict(width=1, color='gray'),\n                        showlegend=False\n                    ), row=start_row + i, col=1)\n\n                    fig.add_trace(go.Scatter(\n                        x=df['timestamp'],\n                        y=df['bollinger_lower'],\n                        mode='lines',\n                        name=f\"{slug} ({outcome}) Lower BB\",\n                        line=dict(width=1, color='gray'),\n                        fill='tonexty',\n                        fillcolor='rgba(128, 128, 128, 0.2)',\n                        showlegend=False\n                    ), row=start_row + i, col=1)\n                else:\n                    print(f\"Data in {file_path} does not contain expected columns 'timestamp' and 'price'\")\n            else:\n                print(f\"File {file_path} does not exist.\")\n\n    def add_final_value_marker(fig, df, row, col, line_name, value_column, secondary_y=False):\n        \"\"\"\n        Adds the final value marker for the specified line on the plot.\n\n        Args:\n            fig: The Plotly figure object.\n            df: The dataframe containing the data.\n            row: The subplot row index.\n            col: The subplot column index.\n            line_name: The name of the line.\n            value_column: The column to plot the final value.\n            secondary_y: Whether the marker should be plotted on the secondary y-axis.\n        \"\"\"\n        if not df.empty and value_column in df.columns:\n            last_value = df.iloc[-1]\n\n            # Add the final value marker (dot)\n            fig.add_trace(go.Scatter(\n                x=[last_value['timestamp']],\n                y=[last_value[value_column]],\n                mode='markers+text',\n                marker=dict(size=10, color='red'),\n                text=[f\"{last_value[value_column]:.4f}\"],\n                textposition=\"middle right\",\n                name=f\"Final {line_name} Value\",\n                showlegend=False\n            ), row=row, col=col, secondary_y=secondary_y)\n\n            # Draw a line extending from the final value to the right margin of the plot\n            fig.add_trace(go.Scatter(\n                x=[last_value['timestamp'], df['timestamp'].max() + pd.Timedelta(days=0.1)],\n                y=[last_value[value_column], last_value[value_column]],\n                mode='lines',\n                line=dict(dash='dash', color='red'),\n                showlegend=False\n            ), row=row, col=col, secondary_y=secondary_y)\n\n            print(f\"'{line_name}' - Final Value: {last_value[value_column]}\")\n\n    def prepare_data(method, side_a_trades, side_b_trades, positions):\n        if method == \"balanced\":\n            side_a_df, side_a_missing_files, side_a_missing_columns = aggregate_side(side_a_trades)\n            side_b_df, side_b_missing_files, side_b_missing_columns = aggregate_side(side_b_trades)\n\n            if side_a_df is None or side_b_df is None or side_a_df.empty or side_b_df.empty:\n                print(f\"Skipping plot due to missing or insufficient data.\")\n                return None, None\n\n            combined_df = pd.merge(side_a_df, side_b_df, on='timestamp', how='outer', suffixes=('_a', '_b'))\n            calculate_arbitrage_balanced(combined_df)\n            num_subplots = 2 + len(side_a_trades) + len(side_b_trades)\n\n        elif method == \"all_no\":\n            positions_df, positions_missing_files, positions_missing_columns = aggregate_positions(positions)\n\n            if positions_df is None or positions_df.empty:\n                print(f\"Skipping plot due to missing or insufficient data.\")\n                return None, None\n\n            combined_df, error = calculate_arbitrage_all_no(positions_df)\n            if error:\n                print(error)\n                return None, None\n\n            num_subplots = 1 + len(positions)\n\n        return combined_df, num_subplots\n\n    def create_subplots_layout(method, num_subplots, trade_name, side_a_trades, side_b_trades, positions):\n        if method == \"balanced\":\n            fig = make_subplots(\n                rows=num_subplots,\n                cols=1,\n                shared_xaxes=True,\n                vertical_spacing=0.02,\n                subplot_titles=(\n                        [f\"Arbitrage Percentage\", f\"{trade_name} - Side A vs. Side B\"] +\n                        [f\"Side A: {slug} ({outcome})\" for slug, outcome in side_a_trades] +\n                        [f\"Side B: {slug} ({outcome})\" for slug, outcome in side_b_trades]\n                ),\n                specs=[[{\"secondary_y\": False}] * 1] +\n                      [[{\"secondary_y\": True}] * 1] +\n                      [[{\"secondary_y\": False}] * 1 for _ in range(num_subplots - 2)]\n            )\n        elif method == \"all_no\":\n            fig = make_subplots(\n                rows=num_subplots,\n                cols=1,\n                shared_xaxes=True,\n                vertical_spacing=0.02,\n                subplot_titles=(\n                        [f\"Arbitrage Percentage\"] +\n                        [f\"Position: {slug} ({outcome})\" for slug, outcome in positions]\n                ),\n                specs=[[{\"secondary_y\": False}] * 1] +\n                      [[{\"secondary_y\": False}] * 1 for _ in range(num_subplots - 1)]\n            )\n        return fig\n\n    def add_balanced_traces(fig, combined_df, side_a_trades, side_b_trades, trade_name):\n        # Calculate Bollinger Bands for Arbitrage Percentage\n        combined_df['rolling_mean'], combined_df['bollinger_upper'], combined_df[\n            'bollinger_lower'] = calculate_bollinger_bands(combined_df['arb_percentage'])\n\n        # Plot Arbitrage Percentage\n        fig.add_trace(go.Scatter(\n            x=combined_df['timestamp'],\n            y=combined_df['arb_percentage'],\n            mode='lines',\n            name='Arbitrage Percentage',\n            line=dict(width=2, color='green'),\n            showlegend=False\n        ), row=1, col=1)\n\n        # Plot Bollinger Bands around Arbitrage Percentage\n        fig.add_trace(go.Scatter(\n            x=combined_df['timestamp'],\n            y=combined_df['bollinger_upper'],\n            mode='lines',\n            name='Bollinger Upper Band',\n            line=dict(width=1, color='gray'),\n            showlegend=False\n        ), row=1, col=1)\n\n        fig.add_trace(go.Scatter(\n            x=combined_df['timestamp'],\n            y=combined_df['bollinger_lower'],\n            mode='lines',\n            name='Bollinger Lower Band',\n            line=dict(width=1, color='gray'),\n            fill='tonexty',\n            fillcolor='rgba(128, 128, 128, 0.2)',\n            showlegend=False\n        ), row=1, col=1)\n\n        # Add the final value marker\n        add_final_value_marker(fig, combined_df, row=1, col=1, line_name=\"Arbitrage Percentage\",\n                               value_column='arb_percentage')\n\n        # Plot cumulative Side A price\n        fig.add_trace(go.Scatter(\n            x=combined_df['timestamp'],\n            y=combined_df['price_a'],\n            mode='lines',\n            name=f'{trade_name} - Side A (Cumulative)',\n            line=dict(width=2, color='blue'),\n            showlegend=False\n        ), row=2, col=1, secondary_y=False)\n\n        # Plot cumulative Side B price\n        fig.add_trace(go.Scatter(\n            x=combined_df['timestamp'],\n            y=combined_df['price_b'],\n            mode='lines',\n            name=f'{trade_name} - Side B (Cumulative)',\n            line=dict(width=2, color='red'),\n            showlegend=False\n        ), row=2, col=1, secondary_y=True)\n\n        # Add final value markers for Side A and Side B\n        add_final_value_marker(fig, combined_df, row=2, col=1, line_name=\"Side A Price\", value_column='price_a')\n        add_final_value_marker(fig, combined_df, row=2, col=1, line_name=\"Side B Price\", value_column='price_b', secondary_y=True)\n\n        # Plot individual slugs for Side A and Side B\n        for i, (slug, outcome) in enumerate(side_a_trades):\n            plot_individual_slugs([(slug, outcome)], fig, start_row=3 + i, color='blue')\n\n        for i, (slug, outcome) in enumerate(side_b_trades):\n            plot_individual_slugs([(slug, outcome)], fig, start_row=3 + len(side_a_trades) + i, color='red')\n\n    def add_all_no_traces(fig, combined_df, positions):\n        # Calculate Bollinger Bands for Arbitrage Percentage\n        combined_df['rolling_mean'], combined_df['bollinger_upper'], combined_df[\n            'bollinger_lower'] = calculate_bollinger_bands(combined_df['arb_percentage'])\n\n        # Plot Arbitrage Percentage\n        fig.add_trace(go.Scatter(\n            x=combined_df['timestamp'],\n            y=combined_df['arb_percentage'],\n            mode='lines',\n            name='Arbitrage Percentage',\n            line=dict(width=2, color='green'),\n            showlegend=False\n        ), row=1, col=1)\n\n        # Plot Bollinger Bands around Arbitrage Percentage\n        fig.add_trace(go.Scatter(\n            x=combined_df['timestamp'],\n            y=combined_df['bollinger_upper'],\n            mode='lines',\n            name='Bollinger Upper Band',\n            line=dict(width=1, color='gray'),\n            showlegend=False\n        ), row=1, col=1)\n\n        fig.add_trace(go.Scatter(\n            x=combined_df['timestamp'],\n            y=combined_df['bollinger_lower'],\n            mode='lines',\n            name='Bollinger Lower Band',\n            line=dict(width=1, color='gray'),\n            fill='tonexty',\n            fillcolor='rgba(128, 128, 128, 0.2)',\n            showlegend=False\n        ), row=1, col=1)\n\n        # Add the final value marker for Arbitrage Percentage\n        add_final_value_marker(fig, combined_df, row=1, col=1, line_name=\"Arbitrage Percentage\",\n                               value_column='arb_percentage')\n\n        # Plot individual slugs for positions\n        for i, (slug, outcome) in enumerate(positions):\n            plot_individual_slugs([(slug, outcome)], fig, start_row=2 + i, color='blue')\n\n    # Main Execution Flow\n    combined_df, num_subplots = prepare_data(method, side_a_trades, side_b_trades, positions)\n\n    # Check if the combined DataFrame is None or empty\n    if combined_df is None or combined_df.empty:\n        print(f\"Skipping plot due to missing or insufficient data for trade: {trade_name}\")\n        return  # Exit the function if no data is available\n\n    combined_df.to_csv('.\/data\/combined_df.csv')\n\n    if combined_df is None:\n        return  # Exit if no data was returned\n\n    fig = create_subplots_layout(method, num_subplots, trade_name, side_a_trades, side_b_trades, positions)\n\n    if method == \"balanced\":\n        # Add traces and plot details for balanced method\n        add_balanced_traces(fig, combined_df, side_a_trades, side_b_trades, trade_name)\n\n    elif method == \"all_no\":\n        # Add traces and plot details for all_no method\n        add_all_no_traces(fig, combined_df, positions)\n\n    last_arb_percentage = combined_df['arb_percentage'].iloc[-1] if not combined_df['arb_percentage'].empty else 'NA'\n    title_with_percentage = f\"<b>{trade_name}<\/b> - Final Arb %: {last_arb_percentage:.2f}\"\n\n    fig.update_layout(\n        title={\n            'text': f\"{title_with_percentage}<br><sup>{subtitle}<\/sup>\",\n            'x': 0.5,\n            'xanchor': 'center',\n            'yanchor': 'top'\n        },\n        xaxis_title='Timestamp',\n        yaxis_title='Cumulative Price',\n        template='plotly_white',\n        height=300 * num_subplots,\n        margin=dict(t=100),\n        showlegend=False  # Ensuring legend is not shown\n    )\n\n    output_file = os.path.join(plot_dir, f\"{trade_name.replace(' ', '_')}.html\")\n    fig.write_html(output_file)\n    print(f\"Trade plot saved to {output_file}\")\n\ndef main():\n    # Step 1: Process each trade and fetch data if necessary\n    for trade in trades:\n        print(f'Processing trade: {trade}')\n\n        # Process side A trades\n        if \"side_a_trades\" in trade:\n            for slug, outcome in trade[\"side_a_trades\"]:\n                process_and_fetch_data(slug, outcome)\n\n        # Process side B trades\n        if \"side_b_trades\" in trade:\n            for slug, outcome in trade[\"side_b_trades\"]:\n                process_and_fetch_data(slug, outcome)\n\n        # Process 'all_no' method positions\n        if trade[\"method\"] == \"all_no\" and \"positions\" in trade:\n            for slug, outcome in trade[\"positions\"]:\n                process_and_fetch_data(slug, outcome)\n\n        # Now proceed to plotting after fetching the data\n        if trade[\"method\"] == \"balanced\":\n            plot_trade_sides(trade[\"trade_name\"], trade[\"subtitle\"], trade[\"side_a_trades\"], trade[\"side_b_trades\"],\n                             method=trade[\"method\"])\n        elif trade[\"method\"] == \"all_no\":\n            plot_trade_sides(trade[\"trade_name\"], trade[\"subtitle\"], positions=trade[\"positions\"],\n                             method=trade[\"method\"])\n        else:\n            raise ValueError(f\"Unknown method '{trade['method']}' for trade '{trade['trade_name']}'.\")\n\n\ndef main_loop(interval_minutes=5):\n    \"\"\"\n    Runs the main function in a loop every `interval_minutes`.\n    \"\"\"\n    while True:\n        main()\n        print(f\"Waiting for {interval_minutes} minutes before next run...\")\n        time.sleep(interval_minutes * 60)  # Convert minutes to seconds\n\n\nif __name__ == \"__main__\":\n    # Run the main loop with the desired interval (default is 10 minutes)\n    main_loop()<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">get_user_trade_prices.py<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This code will organize all your prices paid for your positions and organize them so you can make sure your live trading price is what you expect and profitable. Here is a sample output of the HTML file it will generate.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:image {\"id\":11908,\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} -->\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" width=\"1024\" height=\"340\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-15-1024x340.png\" alt=\"\" class=\"wp-image-11908 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-15-1024x340.png 1024w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-15-300x100.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-15-768x255.png 768w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-15-1536x510.png 1536w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-15-500x166.png 500w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/image-15.png 1697w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/340;\" \/><\/figure>\n<!-- \/wp:image -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport pandas as pd\nimport logging\nimport argparse\nfrom jinja2 import Template\nimport subprocess\nimport json\nimport time\nimport subprocess\nimport time\n# Configure logging\nlogging.basicConfig(level=logging.INFO)\n\n\n\ndef call_get_polygon_data(wallet_id_or_username):\n    \"\"\"\n    Call subprocess to get polygon data by wallet_id or username.\n    \"\"\"\n    try:\n        logging.info(f\"Calling get_polygon_data.py for wallet\/username: {wallet_id_or_username}\")\n        result = subprocess.run(\n            ['python3', 'get_polygon_data.py', '--wallets', wallet_id_or_username],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            check=True,\n            text=True\n        )\n        logging.debug(f\"Subprocess stdout: {result.stdout}\")\n        logging.debug(f\"Subprocess stderr: {result.stderr}\")\n        logging.info(f\"Polygon data retrieval completed for {wallet_id_or_username}.\")\n    except subprocess.CalledProcessError as e:\n        logging.error(f\"Error calling get_polygon_data.py for {wallet_id_or_username}: {e.stderr}\")\n        return False\n    return True\n\ndef load_strategies_from_python():\n    \"\"\"\n    Load strategies from the strategies.py file.\n    \"\"\"\n    try:\n        from strategies import trades\n        return trades\n    except Exception as e:\n        logging.error(f\"Failed to load strategies from strategies.py: {e}\")\n        return []\n\ndef load_user_data(user_data_path):\n    \"\"\"\n    Load user transaction data from Parquet or CSV file.\n    \"\"\"\n    try:\n        if user_data_path.endswith('.parquet'):\n            return pd.read_parquet(user_data_path)\n        elif user_data_path.endswith('.csv'):\n            return pd.read_csv(user_data_path)\n        else:\n            raise ValueError(\"Unsupported file format. Only Parquet and CSV are supported.\")\n    except Exception as e:\n        logging.error(f\"Error loading user data: {e}\")\n        return pd.DataFrame()\n\ndef call_get_user_profile(wallet_id):\n    \"\"\"\n    Call subprocess to get user profile data by wallet_id.\n    \"\"\"\n    if not wallet_id:\n        logging.error(\"No wallet ID provided.\")\n        return None\n\n    try:\n        logging.info(f\"Calling subprocess to fetch user profile for wallet ID: {wallet_id}\")\n        result = subprocess.run(\n            ['python3', 'get_user_profile.py', wallet_id],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            check=True,\n            text=True,\n            timeout=30\n        )\n        logging.debug(f\"Subprocess stdout: {result.stdout}\")\n        logging.debug(f\"Subprocess stderr: {result.stderr}\")\n        user_data = json.loads(result.stdout)\n        return user_data\n\n    except subprocess.TimeoutExpired:\n        logging.error(f\"Subprocess timed out when fetching user profile for wallet ID: {wallet_id}\")\n        return None\n    except subprocess.CalledProcessError as e:\n        logging.error(f\"Subprocess error when fetching user profile for wallet ID {wallet_id}: {e.stderr}\")\n        return None\n    except json.JSONDecodeError as e:\n        logging.error(f\"Failed to parse JSON from subprocess for wallet ID {wallet_id}: {e}\")\n        return None\n\ndef get_username_from_wallet(wallet_id):\n    \"\"\"\n    Fetch username corresponding to a wallet ID.\n    \"\"\"\n    user_data = call_get_user_profile(wallet_id)\n    if user_data and 'username' in user_data:\n        return user_data['username']\n    else:\n        logging.error(f\"No username found for wallet ID: {wallet_id}\")\n        return None\ndef load_user_data(user_data_path):\n    \"\"\"\n    Load user transaction data from Parquet or CSV file.\n    \"\"\"\n    try:\n        if user_data_path.endswith('.parquet'):\n            return pd.read_parquet(user_data_path)\n        elif user_data_path.endswith('.csv'):\n            return pd.read_csv(user_data_path)\n        else:\n            raise ValueError(\"Unsupported file format. Only Parquet and CSV are supported.\")\n    except Exception as e:\n        logging.error(f\"Error loading user data: {e}\")\n        return pd.DataFrame()\n\ndef get_last_price_paid(df, market_slug, outcome):\n    \"\"\"\n    Get the last price paid for a specific market_slug and outcome.\n    \"\"\"\n    filtered_df = df[(df['market_slug'] == market_slug) &amp; (df['outcome'] == outcome)]\n    if not filtered_df.empty:\n        latest_transaction = filtered_df.sort_values(by='timeStamp_erc1155', ascending=False).iloc[0]\n        return latest_transaction['price_paid_per_token']\n    else:\n        return None\ndef calculate_total_prices(positions_with_prices):\n    \"\"\"\n    Calculate the total price for a list of positions.\n    \"\"\"\n    total_price = sum(position['last_price_paid'] for position in positions_with_prices if isinstance(position['last_price_paid'], (int, float)))\n    return total_price\n\ndef calculate_shares(df, market_slug, outcome):\n    \"\"\"\n    Calculate total shares for a given market_slug and outcome.\n    Buys add shares, and sells subtract shares.\n    \"\"\"\n    filtered_df = df[(df['market_slug'] == market_slug) &amp; (df['outcome'] == outcome)]\n    if filtered_df.empty:\n        return None  # Return None if no data is found\n\n    # Sum up the shares based on transaction_type\n    total_shares = filtered_df.apply(\n        lambda row: row['shares'] if row['transaction_type'] == 'buy' else -row['shares'], axis=1).sum()\n\n    return round(total_shares)  # Round to the nearest whole number\n\ndef calculate_shares_to_balance(positions_with_shares):\n    \"\"\"\n    Calculate the 'shares to balance trade' for each position.\n    This will be the difference between the highest number of shares and the shares of the current row.\n    If no valid shares exist, set 'shares_to_balance' to 'No Data'.\n    \"\"\"\n    valid_shares = [pos['shares'] for pos in positions_with_shares if isinstance(pos['shares'], (int, float))]\n\n    if not valid_shares:\n        # If there are no valid shares, set 'shares_to_balance' to 'No Data' for all positions\n        for pos in positions_with_shares:\n            pos['shares_to_balance'] = \"No Data\"\n        return\n\n    max_shares = max(valid_shares)\n\n    for pos in positions_with_shares:\n        if isinstance(pos['shares'], (int, float)):\n            pos['shares_to_balance'] = max_shares - pos['shares']\n        else:\n            pos['shares_to_balance'] = \"No Data\"\n\ndef calculate_total_average_prices(positions_with_prices):\n    \"\"\"\n    Calculate the total of average prices for a list of positions.\n    \"\"\"\n    total_avg_price = sum(position['average_price_paid'] for position in positions_with_prices if isinstance(position['average_price_paid'], (int, float)))\n    return total_avg_price\n\n\n\ndef calculate_average_price(df, market_slug, outcome):\n    \"\"\"\n    Calculate the average price paid for a given market_slug and outcome.\n    Only buy transactions are considered.\n    \"\"\"\n    filtered_df = df[\n        (df['market_slug'] == market_slug) &amp; (df['outcome'] == outcome) &amp; (df['transaction_type'] == 'buy')]\n\n    if filtered_df.empty:\n        return None  # Return None if no buy transactions found\n\n    # Calculate total amount paid and total shares bought\n    total_amount_paid = (filtered_df['price_paid_per_token'] * filtered_df['shares']).sum()\n    total_shares_bought = filtered_df['shares'].sum()\n\n    if total_shares_bought == 0:\n        return None  # Avoid division by zero if no shares bought\n\n    # Calculate the average price paid\n    average_price_paid = total_amount_paid \/ total_shares_bought\n    return round(average_price_paid, 3)  # Return rounded to 3 decimals\n\ndef generate_html_summary(trades, user_data, output_path):\n    \"\"\"\n    Generate an HTML file summarizing the last price paid, total shares, shares to balance, and average price paid for each trade.\n    \"\"\"\n    html_template = \"\"\"\n    \n    \n        <title>Trade Summary<\/title>\n        \n            table { width: 100%; border-collapse: collapse; }\n            th, td { padding: 8px 12px; border: 1px solid #ccc; text-align: left; }\n            th { background-color: #f4f4f4; }\n        \n    \n    \n        <h1>Summary of Last Prices Paid for Trades<\/h1>\n        {% for trade in trades %}\n            <h2>{{ trade.trade_name }}<\/h2>\n            <p>{{ trade.subtitle }}<\/p>\n            {% if trade.total_a is not none or trade.total_b is not none %}\n                <p><strong>\n                    {% if trade.total_a is not none %} Total Price for Side A: {{ '%.3f' % trade.total_a }} {% endif %}\n                    {% if trade.total_b is not none %} Total Price for Side B: {{ '%.3f' % trade.total_b }} {% endif %}\n                    {% if trade.total_price is not none %} Total Price for Both Sides: {{ '%.3f' % trade.total_price }} {% endif %}\n                <\/strong><\/p>\n                <p><strong>\n                    {% if trade.total_avg_a is not none %} Total of Average Prices for Side A: {{ '%.3f' % trade.total_avg_a }} {% endif %}\n                    {% if trade.total_avg_b is not none %} Total of Average Prices for Side B: {{ '%.3f' % trade.total_avg_b }} {% endif %}\n                    {% if trade.total_avg_price is not none %} Total of Average Prices for Both Sides: {{ '%.3f' % trade.total_avg_price }} {% endif %}\n                <\/strong><\/p>\n            {% endif %}\n            <table>\n                <thead>\n                    <tr>\n                        <th>Market Slug<\/th>\n                        <th>Outcome<\/th>\n                        <th>Last Price Paid<\/th>\n                        <th>Shares<\/th>\n                        <th>Shares to Balance Trade<\/th>\n                        <th>Average Price Paid<\/th>\n                    <\/tr>\n                <\/thead>\n                <tbody>\n                    {% for position in trade.positions %}\n                        <tr>\n                            <td>{{ position.slug }}<\/td>\n                            <td>{{ position.outcome }}<\/td>\n                            <td>{{ position.last_price_paid }}<\/td>\n                            <td>{{ position.shares }}<\/td>\n                            <td>{{ position.shares_to_balance }}<\/td>\n                            <td>{{ position.average_price_paid }}<\/td>\n                        <\/tr>\n                    {% endfor %}\n                <\/tbody>\n            <\/table>\n        {% endfor %}\n    \n    \n    \"\"\"\n\n    # Prepare data to be passed to the template\n    processed_trades_complete = []\n    processed_trades_incomplete = []\n\n    for trade in trades:\n        positions_with_prices = []\n        has_missing_data = False\n        total_a = None\n        total_b = None\n        total_avg_a = None\n        total_avg_b = None\n        valid_last_price_paid = False  # Track if there's any valid last price\n\n        if trade.get('positions'):  # Handle \"all_no\" method\n            for slug, outcome in trade['positions']:\n                last_price_paid = get_last_price_paid(user_data, slug, outcome)\n                shares = calculate_shares(user_data, slug, outcome)  # Calculate shares\n                average_price_paid = calculate_average_price(user_data, slug, outcome)  # Calculate average price paid\n\n                if last_price_paid is not None:\n                    valid_last_price_paid = True  # Mark as valid data if any valid price is found\n\n                if last_price_paid is None or shares is None:\n                    has_missing_data = True\n\n                positions_with_prices.append({\n                    'slug': slug,\n                    'outcome': outcome,\n                    'last_price_paid': last_price_paid if last_price_paid is not None else \"No Data\",\n                    'shares': shares if shares is not None else \"No Data\",\n                    'average_price_paid': average_price_paid if average_price_paid is not None else \"No Data\"\n                })\n\n            # If no valid last price paid for all positions, skip the trade\n            if not valid_last_price_paid:\n                continue  # Skip this trade\n\n            # Calculate 'shares to balance trade' for all positions\n            calculate_shares_to_balance(positions_with_prices)\n\n        elif trade.get('side_a_trades') and trade.get('side_b_trades'):  # Handle \"balanced\" method\n            positions_with_prices_a = []\n            positions_with_prices_b = []\n\n            for slug, outcome in trade['side_a_trades']:\n                last_price_paid = get_last_price_paid(user_data, slug, outcome)\n                shares = calculate_shares(user_data, slug, outcome)  # Calculate shares\n                average_price_paid = calculate_average_price(user_data, slug, outcome)  # Calculate average price paid\n\n                if last_price_paid is not None:\n                    valid_last_price_paid = True  # Mark as valid data if any valid price is found\n\n                if last_price_paid is None or shares is None:\n                    has_missing_data = True\n\n                positions_with_prices_a.append({\n                    'slug': slug,\n                    'outcome': outcome,\n                    'last_price_paid': last_price_paid if last_price_paid is not None else \"No Data\",\n                    'shares': shares if shares is not None else \"No Data\",\n                    'average_price_paid': average_price_paid if average_price_paid is not None else \"No Data\"\n                })\n\n            for slug, outcome in trade['side_b_trades']:\n                last_price_paid = get_last_price_paid(user_data, slug, outcome)\n                shares = calculate_shares(user_data, slug, outcome)  # Calculate shares\n                average_price_paid = calculate_average_price(user_data, slug, outcome)  # Calculate average price paid\n\n                if last_price_paid is not None:\n                    valid_last_price_paid = True  # Mark as valid data if any valid price is found\n\n                if last_price_paid is None or shares is None:\n                    has_missing_data = True\n\n                positions_with_prices_b.append({\n                    'slug': slug,\n                    'outcome': outcome,\n                    'last_price_paid': last_price_paid if last_price_paid is not None else \"No Data\",\n                    'shares': shares if shares is not None else \"No Data\",\n                    'average_price_paid': average_price_paid if average_price_paid is not None else \"No Data\"\n                })\n\n            # If no valid last price paid for all positions, skip the trade\n            if not valid_last_price_paid:\n                continue  # Skip this trade\n\n            positions_with_prices = positions_with_prices_a + positions_with_prices_b\n\n            # Calculate 'shares to balance trade' for both sides\n            calculate_shares_to_balance(positions_with_prices_a)\n            calculate_shares_to_balance(positions_with_prices_b)\n\n            total_a = calculate_total_prices(positions_with_prices_a)\n            total_b = calculate_total_prices(positions_with_prices_b)\n\n            # Calculate the total of average prices for Side A and Side B\n            total_avg_a = calculate_total_average_prices(positions_with_prices_a)\n            total_avg_b = calculate_total_average_prices(positions_with_prices_b)\n\n        total_price = calculate_total_prices(positions_with_prices) if positions_with_prices else None\n        total_avg_price = calculate_total_average_prices(positions_with_prices) if positions_with_prices else None\n\n        processed_trade = {\n            'trade_name': trade['trade_name'],\n            'subtitle': trade['subtitle'],\n            'positions': positions_with_prices,\n            'total_a': total_a if total_a else None,\n            'total_b': total_b if total_b else None,\n            'total_price': total_price if total_price else None,\n            'total_avg_a': total_avg_a if total_avg_a else None,\n            'total_avg_b': total_avg_b if total_avg_b else None,\n            'total_avg_price': total_avg_price if total_avg_price else None\n        }\n\n        if has_missing_data:\n            processed_trades_incomplete.append(processed_trade)\n        else:\n            processed_trades_complete.append(processed_trade)\n\n    # Sort trades: Complete trades at the top, incomplete trades at the bottom\n    processed_trades = processed_trades_complete + processed_trades_incomplete\n\n    # Use Jinja2 to render the template\n    template = Template(html_template)\n    rendered_html = template.render(trades=processed_trades)\n\n    # Write to the output HTML file\n    with open(output_path, 'w') as f:\n        f.write(rendered_html)\n\n    logging.info(f\"HTML summary saved to {output_path}\")\n\n\ndef process_wallet_data_for_user(username):\n    \"\"\"\n    Call the first program to process wallet data for a given user.\n    \"\"\"\n    try:\n        command = ['python3', 'first_program.py', '--wallets', username]\n        result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, text=True)\n        logging.info(f\"Wallet data processing completed for user {username}.\")\n        logging.debug(f\"Subprocess output: {result.stdout}\")\n        logging.debug(f\"Subprocess error (if any): {result.stderr}\")\n    except subprocess.CalledProcessError as e:\n        logging.error(f\"Error processing wallet data for {username}: {e.stderr}\")\n        return False\n    return True\n\ndef main(wallet_id_or_username, strategies_file):\n    \"\"\"\n    Main function to process the user trades and strategies.\n    \"\"\"\n    # Handle input to determine if it's a wallet ID or username\n    if wallet_id_or_username is None:\n        logging.info(\"No wallet ID or username provided, defaulting to 'JeremyRWhittaker'.\")\n        username = \"JeremyRWhittaker\"\n        wallet_address = None\n    elif wallet_id_or_username.startswith(\"0x\"):  # It's a wallet address\n        logging.info(f\"Input detected as wallet ID: {wallet_id_or_username}\")\n        wallet_address = wallet_id_or_username\n        # Attempt to fetch the username from the wallet address\n        username = get_username_from_wallet(wallet_address)\n        if username is None:\n            logging.error(f\"Could not resolve username from wallet address: {wallet_address}\")\n            return\n    else:\n        logging.info(f\"Input detected as username: {wallet_id_or_username}\")\n        username = wallet_id_or_username\n        wallet_address = None\n\n    # Check if we found a username\n    if username is None:\n        logging.error(f\"Could not find a valid username for {wallet_id_or_username}. Exiting.\")\n        return\n\n    # Update user's trade data by running get_polygon_data.py as a subprocess\n    try:\n        logging.info(f\"Updating trade data for user: {username} (wallet: {wallet_address})\")\n\n        cmd = [\n            'python', 'get_polygon_data.py',\n            '--wallets', wallet_address or username,  # Use wallet address if available, otherwise username\n            '--skip-leaderboard',\n            '--no-plot'\n        ]\n        subprocess.run(cmd, check=True)\n        logging.info(f\"Successfully updated trade data for user: {username}\")\n    except subprocess.CalledProcessError as e:\n        logging.error(f\"Error updating trade data: {e}\", exc_info=True)\n        return\n\n    # Load user transaction data\n    user_data_file = f'.\/data\/user_trades\/{username}_enriched_transactions.parquet'\n    user_data = load_user_data(user_data_file)\n\n    if user_data.empty:\n        logging.error(f\"No transaction data found for user: {username}. Exiting.\")\n        return\n\n    # Load strategies and generate HTML summary\n    trades = load_strategies_from_python()\n    if not trades:\n        logging.error(f\"No valid strategies found in {strategies_file}. Exiting.\")\n        return\n\n    output_html_file = f'.\/strategies\/{username}_last_traded_price.html'\n    generate_html_summary(trades, user_data, output_html_file)\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"Generate a trade summary HTML for a user.\")\n    parser.add_argument('wallet_id_or_username', nargs='?', default=None,\n                        help=\"Username or wallet ID for which to generate the summary (used to locate transaction file).\")\n    parser.add_argument('strategies_file', nargs='?', default=\".\/data\/strategies.py\",\n                        help=\"Path to the strategies file (Python).\")\n\n    args = parser.parse_args()\n\n    main(args.wallet_id_or_username, args.strategies_file)<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">get_user_profile.py<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This code will scrape polymarket.com for user profile information. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport subprocess\nimport requests\nimport json\nimport zipfile\nimport io\nimport time\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.service import Service\nfrom selenium.webdriver.chrome.options import Options\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.support.ui import WebDriverWait\nfrom selenium.webdriver.support import expected_conditions as EC\nfrom bs4 import BeautifulSoup\nimport logging\nfrom dotenv import load_dotenv\nfrom get_leaderboard_wallet_ids import scrape_wallet_ids\n\n# Load environment variables\nload_dotenv('keys.env')\n\n# Set up logging\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\nlogger = logging.getLogger()\n\n\ndef get_chrome_version():\n    \"\"\"Get the current installed version of Google Chrome.\"\"\"\n    try:\n        version_output = subprocess.check_output(['google-chrome', '--version']).decode('utf-8').strip()\n        version = version_output.split()[-1]\n        return version\n    except subprocess.CalledProcessError:\n        raise Exception(\"Failed to get Chrome version. Ensure Google Chrome is installed and accessible in PATH.\")\n\n\ndef fetch_driver_version(chrome_version, json_url):\n    \"\"\"Fetch the correct ChromeDriver version for the installed Chrome version.\"\"\"\n    response = requests.get(json_url)\n    response.raise_for_status()\n    versions = response.json()['versions']\n\n    for version_info in versions:\n        if chrome_version.startswith(version_info['version'].split('.')[0]):\n            for download in version_info['downloads'].get('chromedriver', []):\n                if download['platform'] == 'linux64':\n                    return download['url'], version_info['version']\n    raise Exception(f\"No matching ChromeDriver version found for Chrome version {chrome_version}\")\n\n\ndef download_and_extract_chromedriver(url, version, extract_path=None):\n    \"\"\"Download and extract ChromeDriver if it doesn't already exist.\"\"\"\n    # Use the directory where this script is located\n    script_dir = os.path.dirname(os.path.abspath(__file__))\n    if extract_path is None:\n        extract_path = os.path.join(script_dir, 'chromedriver')\n\n    version_file_path = os.path.join(extract_path, 'version.txt')\n\n    # Check if the correct version is already downloaded\n    if os.path.exists(extract_path) and os.path.exists(version_file_path):\n        with open(version_file_path, 'r') as version_file:\n            installed_version = version_file.read().strip()\n        if installed_version == version:\n            chromedriver_path = os.path.join(extract_path, 'chromedriver-linux64', 'chromedriver')\n            return chromedriver_path\n\n    if not os.path.exists(extract_path):\n        os.makedirs(extract_path)\n\n    print(f\"Downloading ChromeDriver version {version}...\")\n    response = requests.get(url)\n    response.raise_for_status()\n\n    with zipfile.ZipFile(io.BytesIO(response.content)) as z:\n        z.extractall(extract_path)\n\n    chromedriver_path = os.path.join(extract_path, 'chromedriver-linux64', 'chromedriver')\n\n    if not os.path.exists(chromedriver_path):\n        raise FileNotFoundError(\"The ChromeDriver binary was not found in the extracted files.\")\n\n    os.chmod(chromedriver_path, 0o755)\n\n    return chromedriver_path\n\n\n\n\ndef save_user_data_to_json(user_data, output_path):\n    \"\"\"Save the user data to a JSON file.\"\"\"\n    with open(output_path, 'w') as json_file:\n        json.dump(user_data, json_file, indent=4)\n    logger.info(f\"User data saved to {output_path}\")\n\n\ndef get_chromedriver_path():\n    \"\"\"Get the path to the ChromeDriver based on the current script location.\"\"\"\n    script_dir = os.path.dirname(os.path.abspath(__file__))\n    chromedriver_path = os.path.join(script_dir, 'chromedriver', 'chromedriver-linux64', 'chromedriver')\n    if not os.path.exists(chromedriver_path):\n        raise FileNotFoundError(f\"ChromeDriver not found at {chromedriver_path}\")\n    return chromedriver_path\n\ndef get_user_info(wallet_address):\n    \"\"\"Fetch user info from Polymarket using the wallet address.\"\"\"\n    url = f\"https:\/\/polymarket.com\/profile\/{wallet_address}\"\n\n    chrome_options = Options()\n    chrome_options.add_argument(\"--headless\")\n    chrome_options.add_argument(\"--disable-gpu\")\n    chrome_options.add_argument(\"--no-sandbox\")\n\n    # Use the dynamic ChromeDriver path\n    chromedriver_path = get_chromedriver_path()\n    driver = webdriver.Chrome(service=Service(chromedriver_path), options=chrome_options)\n\n    try:\n        driver.get(url)\n        WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, \"h1.c-ipOUDc\")))\n        soup = BeautifulSoup(driver.page_source, 'html.parser')\n\n        # Extract the username\n        username = soup.select_one(\"h1.c-ipOUDc\").text.strip()\n\n        # Extract positions value\n        positions_value_elements = soup.find_all('p', class_='c-dqzIym c-fcWvkb c-dqzIym-fxyRaa-color-normal c-dqzIym-cTvRMP-spacing-normal c-dqzIym-iIobgq-weight-medium')\n        positions_value = positions_value_elements[0].text.strip() if len(positions_value_elements) &gt; 0 else \"N\/A\"\n\n        # Extract profit\/loss\n        profit_loss = positions_value_elements[1].text.strip() if len(positions_value_elements) &gt; 1 else \"N\/A\"\n\n        # Extract volume traded\n        volume_traded = positions_value_elements[2].text.strip() if len(positions_value_elements) &gt; 2 else \"N\/A\"\n\n        # Extract markets traded\n        markets_traded = positions_value_elements[3].text.strip() if len(positions_value_elements) &gt; 3 else \"N\/A\"\n\n        # Extract joined date\n        joined_date_element = soup.find('p', class_='c-dqzIym c-dqzIym-fxyRaa-color-normal c-dqzIym-cTvRMP-spacing-normal c-dqzIym-jalaKP-weight-normal c-dqzIym-hzzdKO-size-md c-dqzIym-ibGjNZs-css')\n        joined_date = joined_date_element.text.strip() if joined_date_element else \"N\/A\"\n\n        # Extract user description (if exists)\n        profile_description_element = soup.find('p', class_='c-dqzIym c-dqzIym-fxyRaa-color-normal c-dqzIym-cTvRMP-spacing-normal c-dqzIym-jalaKP-weight-normal c-dqzIym-idxllRe-css')\n        profile_description = profile_description_element.text.strip() if profile_description_element else \"No description provided\"\n\n        return {\n            \"username\": username,\n            \"positions_value\": positions_value,\n            \"profit_loss\": profit_loss,\n            \"volume_traded\": volume_traded,\n            \"markets_traded\": markets_traded,\n            \"joined_date\": joined_date,\n            \"wallet_address\": wallet_address,  # Add wallet_address to the dictionary\n            \"profile_description\": profile_description,  # Add profile description to the dictionary\n            \"wallet_address\": wallet_id  # Include the wallet_address\n\n        }\n    finally:\n        driver.quit()\n\n\n\ndef main(wallet_id):\n    \"\"\"\n    Scrapes and returns user data for the given wallet_id.\n    Instead of saving to file, outputs the result as JSON via stdout.\n    \"\"\"\n    try:\n        # Scrape user data\n        logger.info(f\"Scraping data for wallet: {wallet_id}\")\n        user_data = get_user_info(wallet_id)\n\n        # Print user data as JSON to stdout\n        print(json.dumps(user_data, indent=4))\n\n    except Exception as e:\n        logger.error(f\"Failed to scrape data for wallet {wallet_id}: {e}\")\n        print(json.dumps({'error': str(e)}))\n\nif __name__ == \"__main__\":\n    import sys\n\n    if len(sys.argv) &lt; 2:\n        logger.error(&quot;No wallet ID provided.&quot;)\n        sys.exit(1)\n\n    wallet_id = sys.argv[1]  # Expect wallet_id as a command-line argument\n    main(wallet_id)<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">get_trade_slugs_to_parquet.py<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This code will take a trade slug and download all the historical data for it to a parquet file for later analysis. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:enlighter\/codeblock -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport requests\nimport pandas as pd\nimport json\nfrom dotenv import load_dotenv\nimport argparse  # Added for argument parsing\n\n# Load environment variables from a .env file if needed\nload_dotenv()\n\n# Access the environment variables\napi_key = os.getenv('API_KEY')\n\n# Ensure the data and historical directories exist\nos.makedirs('.\/data', exist_ok=True)\nos.makedirs('.\/data\/historical', exist_ok=True)  # Create the historical subfolder\n\nhost = \"https:\/\/clob.polymarket.com\"\n\ndef fetch_timeseries_data(clob_token_id, slug, outcome, fidelity=60, separator=\"_\", save_to_csv=True):\n    \"\"\"\n    Fetches timeseries data for a specific clob token from the Polymarket CLOB API and saves it as a Parquet and optional CSV file.\n    \"\"\"\n    endpoint = f\"{host}\/prices-history\"\n    headers = {\n        \"Authorization\": f\"Bearer {api_key}\",\n        \"Content-Type\": \"application\/json\"\n    }\n\n    params = {\n        \"market\": clob_token_id,\n        \"interval\": \"max\",\n        \"fidelity\": fidelity\n    }\n\n    try:\n        print(f\"Preparing to fetch timeseries data for clobTokenId: {clob_token_id}, slug: '{slug}', outcome: '{outcome}'\")\n\n        response = requests.get(endpoint, headers=headers, params=params)\n        print(f\"Request URL for clobTokenId {clob_token_id}: {response.url}\")\n        response.raise_for_status()\n        data = response.json()\n        history = data.get(\"history\", [])\n\n        if history:\n            print(f\"Retrieved {len(history)} timeseries points for clobTokenId {clob_token_id}.\")\n\n            # Convert to DataFrame\n            df = pd.DataFrame(history)\n            df['timestamp'] = pd.to_datetime(df['t'], unit='s', utc=True)\n            df = df[['timestamp', 'p']]  # Keep only relevant columns\n            df.rename(columns={'p': 'price'}, inplace=True)\n\n            # Sanitize file names\n            sanitized_slug = sanitize_filename(slug, separator)\n            sanitized_outcome = sanitize_filename(outcome, separator)\n\n            print(f\"Sanitized filename: Original: '{slug}', Sanitized: '{sanitized_slug}'\")\n            print(f\"Sanitized outcome: Original: '{outcome}', Sanitized: '{sanitized_outcome}'\")\n\n            # Save to Parquet in the historical subfolder (default)\n            parquet_filename = f\".\/data\/historical\/{sanitized_slug}{separator}{sanitized_outcome}.parquet\"\n            df.to_parquet(parquet_filename, index=False)\n            print(f\"Data saved to {parquet_filename} (Parquet format)\")\n\n            # Save to CSV if the flag is set to True\n            if save_to_csv:\n                csv_filename = f\".\/data\/historical\/{sanitized_slug}{separator}{sanitized_outcome}.csv\"\n                df.to_csv(csv_filename, index=False)\n                print(f\"Data saved to {csv_filename} (CSV format)\")\n\n        else:\n            print(f\"No timeseries data returned for clobTokenId {clob_token_id}.\")\n        return history\n\n    except requests.exceptions.HTTPError as http_err:\n        print(f\"HTTP error occurred: {http_err}\")\n    except Exception as err:\n        print(f\"An error occurred: {err}\")\n\n    return []\n\ndef sanitize_filename(filename, separator=\"_\"):\n    \"\"\"\n    Sanitizes a string to be used as a safe filename and replaces spaces with the specified separator.\n    \"\"\"\n    keep_characters = (' ', '.', '_', '-')\n    sanitized = \"\".join(c for c in filename if c.isalnum() or c in keep_characters).rstrip()\n    return sanitized.replace(' ', separator)\n\nif __name__ == \"__main__\":\n    # Use argparse to accept command-line arguments\n    parser = argparse.ArgumentParser(description='Fetch and save timeseries data for Polymarket CLOB token.')\n    parser.add_argument('token_id', type=str, help='The CLOB token ID')\n    parser.add_argument('market_slug', type=str, help='The market slug')\n    parser.add_argument('outcome', type=str, help='The outcome (Yes or No)')\n\n    args = parser.parse_args()\n\n    # Fetch the timeseries data with provided arguments\n    fetch_timeseries_data(args.token_id, args.market_slug, args.outcome, fidelity=1, save_to_csv=True)\n<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">get_order_book.py<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This gets the order book from Polymarket.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:enlighter\/codeblock -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport json\nimport logging\nimport pandas as pd\nfrom py_clob_client.client import ClobClient\nfrom strategies import trades  # Import the trades list\nfrom dotenv import load_dotenv\n\n\n# Access the environment variables\napi_key = os.getenv('API_KEY')\n\n# Initialize the ClobClient\nhost = \"https:\/\/clob.polymarket.com\"\nchain_id = 137  # Polygon Mainnet\nclient = ClobClient(host, key=api_key, chain_id=chain_id)\n\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n\n\ndef load_market_lookup():\n    \"\"\"Load the market lookup JSON to map slugs to token IDs.\"\"\"\n    with open('.\/data\/market_lookup.json', 'r') as f:\n        return json.load(f)\n\ndef fetch_and_save_order_book(token_id, market_id, slug, outcome):\n    \"\"\"\n    Fetch the live order book for a given token ID and save it to a CSV file.\n\n    Args:\n        token_id (str): The token ID of the market.\n        market_id (str): The market ID associated with the token.\n        slug (str): The slug name of the market.\n        outcome (str): The outcome ('Yes' or 'No') for the market.\n    \"\"\"\n    try:\n        order_book = client.get_order_book(token_id)\n        if not hasattr(order_book, 'bids') or not hasattr(order_book, 'asks'):\n            logging.error(f\"Order book structure is not as expected for token_id: {token_id}\")\n            return\n\n        book_data = []\n        for side, orders in [('asks', order_book.asks), ('bids', order_book.bids)]:\n            for order in orders:\n                book_data.append({\n                    'market_id': market_id,\n                    'asset_id': order_book.asset_id,\n                    'price': float(order.price),\n                    'size': float(order.size),\n                    'side': 'ask' if side == 'asks' else 'bid'\n                })\n\n        df = pd.DataFrame(book_data)\n        if not df.empty:\n            output_dir = \".\/data\/book_data\"\n            os.makedirs(output_dir, exist_ok=True)\n            file_name = f\"{slug}_{outcome}.csv\"  # Use the slug and outcome for the file name\n            output_path = os.path.join(output_dir, file_name)\n            df.to_csv(output_path, index=False)\n            logging.info(f\"Book data for {slug} ({outcome}) saved to {output_path}\")\n        else:\n            logging.warning(f\"No data found for token_id: {token_id}\")\n\n    except Exception as e:\n        logging.error(f\"Failed to fetch or save order book for token_id: {token_id}, error: {e}\")\n\n\ndef update_books_for_trades():\n    \"\"\"Update order book data for the token IDs mentioned in the trades list.\"\"\"\n    market_lookup = load_market_lookup()\n    slug_to_market = {market_data['market_slug']: market_data for market_data in market_lookup.values()}\n\n    for trade in trades:\n        positions = trade.get('positions', []) + trade.get('side_a_trades', []) + trade.get('side_b_trades', [])\n        for slug, outcome in positions:\n            market_data = slug_to_market.get(slug)\n            if not market_data:\n                logging.warning(f\"Market slug {slug} not found in market lookup.\")\n                continue\n\n            outcome_lower = outcome.lower()\n            token_entry = next((token for token in market_data['tokens'] if token['outcome'].lower() == outcome_lower), None)\n\n            if token_entry:\n                token_id = token_entry['token_id']\n                market_id = market_data.get('market_slug')\n                logging.info(f\"Fetching order book for market_id: {market_id}, token_id: {token_id}\")\n                fetch_and_save_order_book(token_id, market_id, slug, outcome)\n            else:\n                logging.warning(f\"Token ID not found for slug: {slug} and outcome: {outcome}\")\n\n\n\nif __name__ == \"__main__\":\n    update_books_for_trades()\n<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:paragraph -->\n<p>get_live_price.py<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>This gets the live price from Polymarket.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport time\nimport logging\nfrom dotenv import load_dotenv\nfrom py_clob_client.client import ClobClient\n\n# Set up logging\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')\nlogger = logging.getLogger()\n\n# Load environment variables from the .env file\nload_dotenv()\n\n# Access the environment variables\nAPI_KEY = os.getenv('API_KEY')\nhost = \"https:\/\/clob.polymarket.com\"\nchain_id = 137  # Polygon Mainnet\n\nprint(f\"Using API key: {API_KEY}\")\n\n# Initialize the ClobClient\nclient = ClobClient(host, chain_id=chain_id)\n\n# Dictionary to cache live prices\nlive_price_cache = {}\nCACHE_DURATION = 60  # Cache live prices for 1 minute\n\n\ndef get_live_price(token_id):\n    \"\"\"\n    Fetch the live price for a given token ID.\n\n    Args:\n        token_id (str): The token ID for which the live price is being requested.\n\n    Returns:\n        float: The live price for the given token ID.\n    \"\"\"\n    cache_key = f\"{token_id}\"\n    current_time = time.time()\n\n    # Check if the price is in the cache and still valid\n    if cache_key in live_price_cache:\n        cached_price, timestamp = live_price_cache[cache_key]\n        if current_time - timestamp &lt; CACHE_DURATION:\n            logger.info(f&quot;Returning cached price for {cache_key}: {cached_price}&quot;)\n            return cached_price\n        else:\n            logger.info(f&quot;Cache expired for {cache_key}. Fetching a new price.&quot;)\n\n    # Fetch new price from the API\n    try:\n        response = client.get_last_trade_price(token_id=token_id)\n        price = response.get(&#039;price&#039;)\n\n        # Cache the price with the current timestamp\n        live_price_cache[cache_key] = (price, current_time)\n        logger.info(f&quot;Fetched live price for {cache_key}: {price}&quot;)\n        return price\n    except Exception as e:\n        logger.error(f&quot;Failed to fetch live price for token {token_id}: {str(e)}&quot;)\n        return None\n\n\n# If this script is executed directly, it can take command-line arguments to test the live price retrieval\nif __name__ == &quot;__main__&quot;:\n    import sys\n\n    if len(sys.argv) &lt; 2:\n        print(&quot;Usage: python get_live_price.py \")\n        sys.exit(1)\n\n    token_id = sys.argv[1]\n\n    live_price = get_live_price(token_id)\n    if live_price is not None:\n        print(f\"Live price for token {token_id}: {live_price}\")\n    else:\n        print(f\"Could not fetch the live price for token {token_id}.\")\n<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:paragraph -->\n<p>get_leaderboard_wallet_ids.py<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>This will scrape Polymarket.com for all the leaderboard wallet ids.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport time\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.service import Service\nfrom selenium.webdriver.chrome.options import Options\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.support.ui import WebDriverWait\nfrom selenium.webdriver.support import expected_conditions as EC\nfrom bs4 import BeautifulSoup\nfrom dotenv import load_dotenv\nimport logging\nimport json\nimport argparse\n\n# Load environment variables\nload_dotenv('keys.env')\n\n# Set up logging\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\nlogger = logging.getLogger()\n\n\ndef get_chromedriver_path():\n    \"\"\"Get the path to the ChromeDriver based on the current script location.\"\"\"\n    script_dir = os.path.dirname(os.path.abspath(__file__))\n    chromedriver_path = os.path.join(script_dir, 'chromedriver', 'chromedriver-linux64', 'chromedriver')\n    if not os.path.exists(chromedriver_path):\n        raise FileNotFoundError(f\"ChromeDriver not found at {chromedriver_path}\")\n    return chromedriver_path\n\n\ndef scrape_wallet_ids(leaderboard_type='volume', time_period='Day'):\n    \"\"\"Scrape wallet IDs from the leaderboard by leaderboard type (volume or profit) and time period (Day, Week, Month, All).\"\"\"\n    url = \"https:\/\/polymarket.com\/leaderboard\"  # Replace with the actual leaderboard URL\n\n    # Initialize the WebDriver\n    chrome_options = Options()\n    chrome_options.add_argument(\"--headless\")\n    chrome_options.add_argument(\"--disable-gpu\")\n    chrome_options.add_argument(\"--no-sandbox\")\n\n    driver = webdriver.Chrome(service=Service(get_chromedriver_path()), options=chrome_options)\n    driver.get(url)\n\n    wallet_ids = set()  # Use a set to avoid duplicates\n\n    try:\n        # Wait until the leaderboard is loaded\n        WebDriverWait(driver, 10).until(\n            EC.presence_of_element_located((By.CSS_SELECTOR, \".c-dhzjXW\"))\n        )\n\n        # Click on the appropriate tab for leaderboard type (Volume or Profit)\n        logger.info(f\"Clicking on {leaderboard_type.capitalize()} leaderboard tab.\")\n\n        if leaderboard_type == 'volume':\n            leaderboard_tab_xpath = \"\/\/p[text()='Volume']\"\n        elif leaderboard_type == 'profit':\n            leaderboard_tab_xpath = \"\/\/p[text()='Profit']\"\n        else:\n            logger.error(\"Invalid leaderboard type provided.\")\n            driver.quit()\n            return list(wallet_ids)\n\n        leaderboard_tab_element = WebDriverWait(driver, 10).until(\n            EC.element_to_be_clickable((By.XPATH, leaderboard_tab_xpath))\n        )\n        leaderboard_tab_element.click()\n        time.sleep(2)  # Wait for the content to load after clicking\n\n        # Parse the page content with BeautifulSoup\n        soup = BeautifulSoup(driver.page_source, 'html.parser')\n\n        # Extract wallet IDs\n        for a_tag in soup.find_all('a', href=True):\n            href = a_tag['href']\n            if href.startswith('\/profile\/'):\n                wallet_id = href.split('\/')[-1]\n                wallet_ids.add(wallet_id)  # Add to the set to avoid duplicates\n\n        logger.info(f\"Extracted wallet IDs from {leaderboard_type.capitalize()} leaderboard.\")\n\n    except Exception as e:\n        logger.error(f\"An error occurred while processing {leaderboard_type.capitalize()} leaderboard: {e}\")\n    finally:\n        driver.quit()\n\n    return list(wallet_ids)\n\n\ndef main():\n    # Set up argument parser\n    parser = argparse.ArgumentParser(description=\"Scrape the leaderboard for top volume or top profit users.\")\n    parser.add_argument('--top-volume', action='store_true', help=\"Scrape the top volume leaderboard\")\n    parser.add_argument('--top-profit', action='store_true', help=\"Scrape the top profit leaderboard\")\n\n    args = parser.parse_args()\n\n    wallet_ids = []\n\n    # Call the scrape_wallet_ids function based on the flags passed\n    if args.top_volume:\n        logger.info(\"Scraping top volume leaderboard.\")\n        wallet_ids.extend(scrape_wallet_ids(leaderboard_type='volume'))\n    if args.top_profit:\n        logger.info(\"Scraping top profit leaderboard.\")\n        wallet_ids.extend(scrape_wallet_ids(leaderboard_type='profit'))\n\n    # Output wallet IDs as JSON\n    print(json.dumps(wallet_ids))\n\n\nif __name__ == \"__main__\":\n    main()<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">get_all_historical_data.py<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This code will get all the historical data for a token id and outcome.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import os\nimport requests\nimport pandas as pd\nimport time  # To add delays if necessary\nfrom dotenv import load_dotenv\nimport ast  # Import ast for safe evaluation of string to literal\n\n# Load environment variables\nload_dotenv()\n\n# Ensure directories exist\nos.makedirs('.\/data', exist_ok=True)\nos.makedirs('.\/data\/historical', exist_ok=True)\n\n# Constants\nAPI_KEY = os.getenv('API_KEY')\nHOST = \"https:\/\/clob.polymarket.com\"\nDATA_FILE = '.\/data\/markets_data.csv'  # CSV file instead of JSON\n\n# Logging utility\ndef log_message(message, level=\"INFO\"):\n    print(f\"{level}: {message}\")\n\n# Load market data from CSV file\ndef load_market_data_from_file(filepath):\n    try:\n        market_data = pd.read_csv(filepath)\n        log_message(f\"Loaded data for {len(market_data)} markets from {filepath}.\")\n        return market_data\n    except Exception as e:\n        log_message(f\"Error loading market data from file: {e}\", level=\"ERROR\")\n        return pd.DataFrame()\n\n# Function to fetch time series data for a specific token_id\ndef fetch_timeseries_data(token_id, slug, outcome, fidelity=60, separator=\"_\", save_to_csv=True):\n    \"\"\"\n    Fetch timeseries data for a specific clob token from the Polymarket CLOB API and save it as Parquet and CSV files.\n    Retrieves data at a minute-level fidelity.\n    Skips requests for empty token_id or outcome.\n    \"\"\"\n    if not token_id or not outcome:  # Check if token_id or outcome is empty\n        log_message(f\"Skipping fetch due to empty token_id or outcome for market slug: '{slug}'\", level=\"WARNING\")\n        return []\n\n    endpoint = f\"{HOST}\/prices-history\"\n    headers = {\n        \"Authorization\": f\"Bearer {API_KEY}\",\n        \"Content-Type\": \"application\/json\"\n    }\n\n    params = {\n        \"market\": token_id,\n        \"interval\": \"max\",  # Get the maximum interval available which could potentially be minute-level data\n        \"fidelity\": fidelity  # Request for minute-level granularity\n    }\n\n    try:\n        log_message(f\"Fetching timeseries data for token_id: {token_id}, slug: '{slug}', outcome: '{outcome}'\")\n        response = requests.get(endpoint, headers=headers, params=params)\n        log_message(f\"Request URL: {response.url}\")\n        log_message(f\"Response Status Code: {response.status_code}\")\n\n        response.raise_for_status()\n        data = response.json()\n        history = data.get(\"history\", [])\n\n        if history:\n            log_message(f\"Retrieved {len(history)} timeseries points for token_id: {token_id}.\")\n            df = pd.DataFrame(history)\n            df['timestamp'] = pd.to_datetime(df['t'], unit='s', utc=True)\n            df = df[['timestamp', 'p']].rename(columns={'p': 'price'})\n\n            sanitized_slug = sanitize_filename(slug, separator)\n            sanitized_outcome = sanitize_filename(outcome, separator)\n\n            parquet_filename = f\".\/data\/historical\/{sanitized_slug}{separator}{sanitized_outcome}.parquet\"\n            csv_filename = f\".\/data\/historical\/{sanitized_slug}{separator}{sanitized_outcome}.csv\"\n\n            df.to_parquet(parquet_filename, index=False)\n            log_message(f\"Data saved to {parquet_filename} (Parquet format)\")\n\n            if save_to_csv:\n                df.to_csv(csv_filename, index=False)\n                log_message(f\"Data saved to {csv_filename} (CSV format)\")\n\n            return history\n        else:\n            log_message(f\"No data returned for token_id: {token_id}.\")\n\n    except requests.exceptions.HTTPError as http_err:\n        log_message(f\"HTTP error occurred for token_id {token_id}: {http_err}\", level=\"ERROR\")\n    except Exception as err:\n        log_message(f\"An error occurred for token_id {token_id}: {err}\", level=\"ERROR\")\n\n    return []\n\n\n# Utility function to sanitize filenames\ndef sanitize_filename(filename, separator=\"_\"):\n    keep_characters = (' ', '.', '_', '-')\n    sanitized = \"\".join(c for c in filename if c.isalnum() or c in keep_characters).rstrip()\n    return sanitized.replace(' ', separator)\n\n# Process market data\ndef process_market_data():\n    market_data = load_market_data_from_file(DATA_FILE)\n    # Correctly use .str accessor to apply .upper()\n    open_markets = market_data[market_data['closed'].astype(str).str.strip().str.upper() == \"FALSE\"]\n\n    for _, market in open_markets.iterrows():\n        slug = market['market_slug']\n        if pd.notna(market['tokens']):\n            try:\n                tokens = ast.literal_eval(market['tokens'])  # Safely evaluate the string to a list\n                for token in tokens:\n                    token_id = token['token_id']\n                    outcome = token['outcome']\n                    fetch_timeseries_data(token_id, slug, outcome, fidelity=60)\n            except ValueError as e:\n                log_message(f\"Failed to parse tokens for market {slug}: {e}\", level=\"ERROR\")\n\nif __name__ == \"__main__\":\n    process_market_data()\n<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">create_markets_data_csv.py  | generate_markets_data_csv.py | generate_market_lookup_json.py<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>These programs essentially download all the clob\/glob market data so you can put it in a json file for cross referencing tokens to slugs\/outcomes etc. <\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import csv\nimport json\nfrom py_clob_client.client import ClobClient\nfrom dotenv import load_dotenv\nimport os\nfrom py_clob_client.clob_types import OpenOrderParams\n\n# Load environment variables\nload_dotenv(\"keys.env\")\napi_key = os.getenv('API_KEY')\n\n# Replace with your actual host and chain ID\nhost = \"https:\/\/clob.polymarket.com\"\nchain_id = 137  # Polygon Mainnet\n\n# Initialize the client with only the host, key, and chain_id\nclient = ClobClient(\n    host,\n    chain_id=chain_id\n)\n\n# Initialize variables for pagination\nmarkets_list = []\nnext_cursor = None\n\n# Fetch all available markets using pagination\nwhile True:\n    try:\n        # Print the cursor value for debugging\n        print(f\"Fetching markets with next_cursor: {next_cursor}\")\n\n        # Make the API call based on the cursor value\n        if next_cursor is None:\n            response = client.get_markets()\n        else:\n            response = client.get_markets(next_cursor=next_cursor)\n\n        # Print the raw response for debugging\n        print(f\"API Response: {json.dumps(response, indent=2)}\")\n\n        # Check if the response is successful and contains data\n        if 'data' not in response:\n            print(\"No data found in response.\")\n            break\n\n        markets_list.extend(response['data'])\n        next_cursor = response.get(\"next_cursor\")\n\n        # Exit loop if there's no next_cursor indicating no more data to fetch\n        if not next_cursor:\n            break\n\n    except Exception as e:\n        # Print the exception details for debugging\n        print(f\"Exception occurred: {e}\")\n        print(f\"Exception details: {e.__class__.__name__}\")\n        print(f\"Error message: {e.args}\")\n        break\n\n# Debugging step: Print out the raw data to understand its structure\nprint(\"Raw Market Data:\")\nprint(json.dumps(markets_list, indent=2))\n\n# Dynamically extract all keys from the markets to create the CSV columns\ncsv_columns = set()\nfor market in markets_list:\n    csv_columns.update(market.keys())\n    # Also include nested keys like tokens\n    if 'tokens' in market:\n        csv_columns.update({f\"token_{key}\" for token in market['tokens'] for key in token.keys()})\n\ncsv_columns = sorted(csv_columns)  # Sort columns alphabetically for consistency\n\n# Writing to CSV\ncsv_file = \".\/data\/markets_data.csv\"\ntry:\n    with open(csv_file, 'w', newline='') as csvfile:\n        writer = csv.DictWriter(csvfile, fieldnames=csv_columns)\n        writer.writeheader()\n        for market in markets_list:\n            row = {}\n            for key in csv_columns:\n                # Handling nested 'tokens' structure\n                if key.startswith(\"token_\"):\n                    token_key = key[len(\"token_\"):]\n                    row[key] = ', '.join([str(token.get(token_key, 'N\/A')) for token in market.get('tokens', [])])\n                else:\n                    row[key] = market.get(key, 'N\/A')\n            writer.writerow(row)\n    print(f\"Data has been written to {csv_file} successfully.\")\nexcept IOError as e:\n    print(f\"Error writing to CSV: {e}\")<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import pandas as pd\nimport json\nimport ast\n\ndef create_market_lookup(csv_file, output_json_file):\n    \"\"\"Create a JSON lookup from a CSV file for condition_id to description, market_slug, and tokens.\"\"\"\n\n    # Read the CSV file\n    df = pd.read_csv(csv_file)\n\n    # Drop duplicate condition_ids, keeping the first occurrence\n    lookup_df = df[['condition_id', 'description', 'market_slug', 'tokens']].drop_duplicates(subset='condition_id')\n\n    # Initialize an empty dictionary to store the lookup\n    lookup_dict = {}\n\n    for index, row in lookup_df.iterrows():\n        # Parse the tokens column (it's assumed to be a string representation of a list of dictionaries)\n        tokens_list = ast.literal_eval(row['tokens'])\n\n        # Extract token_id and outcome from each token\n        tokens_info = [{\"token_id\": token[\"token_id\"], \"outcome\": token[\"outcome\"]} for token in tokens_list]\n\n        # Add the information to the lookup dictionary\n        lookup_dict[row['condition_id']] = {\n            \"description\": row['description'],\n            \"market_slug\": row['market_slug'],\n            \"tokens\": tokens_info\n        }\n\n    # Save the dictionary as a JSON file\n    with open(output_json_file, 'w') as json_file:\n        json.dump(lookup_dict, json_file, indent=4)\n\n    print(f\"Lookup JSON file created: {output_json_file}\")\n\n# Usage\ncsv_file = '.\/data\/markets_data.csv'  # Replace with your actual file path\noutput_json_file = '.\/data\/market_lookup.json'\ncreate_market_lookup(csv_file, output_json_file)\n\n\n\ndef query_description_by_keyword(lookup_json, keyword):\n    with open(lookup_json, 'r') as json_file:\n        lookup_dict = json.load(json_file)\n\n    results = {cond_id: info for cond_id, info in lookup_dict.items() if keyword.lower() in info['description'].lower()}\n    return results\n\n\ndef get_market_slug_by_condition_id(lookup_json, condition_id):\n    with open(lookup_json, 'r') as json_file:\n        lookup_dict = json.load(json_file)\n\n    return lookup_dict.get(condition_id, {}).get('market_slug')\n\n\n\n# Usage\ncsv_file = '.\/data\/markets_data.csv'\noutput_json_file = '.\/data\/market_lookup.json'\ncreate_market_lookup(csv_file, output_json_file)\n\n\n# Usage examples\n# print(query_description_by_keyword('market_lookup.json', 'Trump'))\nprint(get_market_slug_by_condition_id('.\/data\/market_lookup.json',\n                                      '0x84dfb8b5cac6356d4ac7bb1da55bb167d0ef65d06afc2546389630098cc467e9'))\n<\/pre>\n<!-- \/wp:enlighter\/codeblock -->\n\n<!-- wp:enlighter\/codeblock {\"language\":\"python\"} -->\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import csv\nimport json\nimport os\nimport time\n\nfrom datetime import datetime, timedelta\nfrom py_clob_client.client import ClobClient\nfrom py_clob_client.clob_types import OpenOrderParams\nfrom py_clob_client.exceptions import PolyApiException\n\n\n# Access the environment variables\napi_key = os.getenv('API_KEY')\n\n# Replace with your actual host and chain ID\nhost = \"https:\/\/clob.polymarket.com\"\nchain_id = 137  # Polygon Mainnet\nmapping_file_path = \"old\/condition_id_question_mapping.json\"\n\n# Initialize the client with only the host, key, and chain_id\nclient = ClobClient(\n    host,\n    key=api_key,\n    chain_id=chain_id\n)\n\n\ndef fetch_all_markets(client):\n    markets_list = []\n    next_cursor = None\n    while True:\n        try:\n            print(f\"Fetching markets with next_cursor: {next_cursor}\")\n            if next_cursor is None:\n                response = client.get_markets()\n            else:\n                response = client.get_markets(next_cursor=next_cursor)\n\n            print(f\"API Response: {json.dumps(response, indent=2)}\")\n            if 'data' not in response:\n                print(\"No data found in response.\")\n                break\n\n            markets_list.extend(response['data'])\n            next_cursor = response.get(\"next_cursor\")\n            if not next_cursor:\n                break\n\n        except Exception as e:\n            print(f\"Exception occurred: {e}\")\n            print(f\"Exception details: {e.__class__.__name__}\")\n            print(f\"Error message: {e.args}\")\n            break\n\n    print(\"Raw Market Data:\")\n    print(json.dumps(markets_list, indent=2))\n\n    return markets_list\n\ndef extract_specific_market_details(client, condition_id):\n    try:\n        market_data = client.get_market(condition_id=condition_id)\n        if market_data:\n            print(\"Market Data Found:\")\n            print(json.dumps(market_data, indent=2))\n            return market_data\n        else:\n            print(\"Market data not found or invalid condition_id.\")\n    except Exception as e:\n        print(f\"Exception occurred: {e}\")\n        print(f\"Exception details: {e.__class__.__name__}\")\n        print(f\"Error message: {e.args}\")\n\ndef write_markets_to_csv(markets_list, csv_file=\".\/data\/markets_data.csv\"):\n    csv_columns = set()\n    for market in markets_list:\n        csv_columns.update(market.keys())\n        if 'tokens' in market:\n            csv_columns.update({f\"token_{key}\" for token in market['tokens'] for key in token.keys()})\n\n    csv_columns = sorted(csv_columns)\n\n    try:\n        with open(csv_file, 'w', newline='') as csvfile:\n            writer = csv.DictWriter(csvfile, fieldnames=csv_columns)\n            writer.writeheader()\n            for market in markets_list:\n                row = {}\n                for key in csv_columns:\n                    if key.startswith(\"token_\"):\n                        token_key = key[len(\"token_\"):]\n                        row[key] = ', '.join([str(token.get(token_key, 'N\/A')) for token in market.get('tokens', [])])\n                    else:\n                        row[key] = market.get(key, 'N\/A')\n                writer.writerow(row)\n        print(f\"Data has been written to {csv_file} successfully.\")\n    except IOError as e:\n        print(f\"Error writing to CSV: {e}\")\n\n\ndef fetch_market_prices(client, condition_id):\n    try:\n        market_data = client.get_market(condition_id=condition_id)\n        if market_data:\n            # Extract Yes and No prices\n            yes_price = None\n            no_price = None\n\n            for token in market_data.get('tokens', []):\n                if token['outcome'].lower() == 'yes':\n                    yes_price = token['price']\n                elif token['outcome'].lower() == 'no':\n                    no_price = token['price']\n\n            if yes_price is not None and no_price is not None:\n                print(f\"Market: {market_data['question']}\")\n                print(f\"Yes Price: {yes_price}\")\n                print(f\"No Price: {no_price}\")\n            else:\n                print(\"Yes or No price not found in the market data.\")\n        else:\n            print(\"Market data not found or invalid condition_id.\")\n    except Exception as e:\n        print(f\"Exception occurred: {e}\")\n        print(f\"Exception details: {e.__class__.__name__}\")\n        print(f\"Error message: {e.args}\")\n\n# Assuming the ClobClient and the necessary initialization is done above this point\n# Function to fetch market data based on condition_id and outcome\ndef get_market_price(condition_id, outcome):\n    try:\n        market_data = client.get_market(condition_id=condition_id)\n        if market_data and 'tokens' in market_data:\n            for token in market_data['tokens']:\n                if token['outcome'].lower() == outcome.lower():\n                    return token['price']\n        print(f\"Price not found for condition_id: {condition_id} with outcome: {outcome}\")\n    except Exception as e:\n        print(f\"Exception occurred while fetching market data: {e}\")\n    return None\n\n# Function to calculate arbitrage percentage between two lists of trades\n# Assuming the ClobClient and the necessary initialization is done above this point\n# Function to fetch market data based on condition_id and outcome\ndef get_market_data(condition_id):\n    try:\n        market_data = client.get_market(condition_id=condition_id)\n        if market_data:\n            return market_data\n    except Exception as e:\n        print(f\"Exception occurred while fetching market data: {e}\")\n    return None\n\n\n# Function to fetch all market data and create a mapping\ndef create_condition_id_question_mapping():\n    markets_list = fetch_all_markets(client)\n    if not markets_list:\n        print(\"No markets data available to create the mapping.\")\n        return\n\n    # Create the dictionary mapping\n    condition_id_question_map = {market['condition_id']: market['question'] for market in markets_list}\n\n    # Save the mapping to a file\n    with open(mapping_file_path, 'w') as f:\n        json.dump(condition_id_question_map, f, indent=2)\n    print(f\"Condition ID to Question mapping saved to {mapping_file_path}\")\n\n# Function to check if the mapping file needs to be updated\ndef update_mapping_if_needed():\n    if os.path.exists(mapping_file_path):\n        file_mod_time = datetime.fromtimestamp(os.path.getmtime(mapping_file_path))\n        if datetime.now() - file_mod_time &gt; timedelta(days=1):\n            print(\"Updating the mapping file.\")\n            create_condition_id_question_mapping()\n        else:\n            print(\"Mapping file is up-to-date.\")\n    else:\n        print(\"Mapping file does not exist, creating new one.\")\n        create_condition_id_question_mapping()\n\n# Function to load the condition_id to question mapping\ndef load_condition_id_question_mapping():\n    if os.path.exists(mapping_file_path):\n        with open(mapping_file_path, 'r') as f:\n            return json.load(f)\n    else:\n        print(\"Mapping file not found. Please update the mapping first.\")\n        return {}\n\n# Function to search for keywords in questions\ndef search_questions(keywords):\n    update_mapping_if_needed()\n    condition_id_question_map = load_condition_id_question_mapping()\n    if not condition_id_question_map:\n        print(\"No mapping data available.\")\n        return []\n\n    # Ensure all keywords must be found in the question\n    def all_keywords_in_question(question, keywords):\n        return all(keyword.lower() in question.lower() for keyword in keywords)\n\n    matched_items = [\n        (condition_id, question)\n        for condition_id, question in condition_id_question_map.items()\n        if all_keywords_in_question(question, keywords)\n    ]\n\n    # Print matched condition IDs along with their corresponding questions\n    print(f\"Matched Condition IDs and Questions for keywords '{', '.join(keywords)}':\")\n    for condition_id, question in matched_items:\n        print(f\"Condition ID: {condition_id}\")\n        print(f\"Question: {question}\\n\")\n\n    return matched_items\n\n\ndef calculate_multiple_arbitrage_opportunities(arb_opportunities):\n    results = []\n\n    for opportunity in arb_opportunities:\n        strategy_name, side_a_ids, side_b_ids, side_a_outcome, side_b_outcome = opportunity\n\n        side_a_info = []\n        side_b_info = []\n\n        side_a_cost = 0\n        side_b_cost = 0\n\n        for condition_id in side_a_ids:\n            market_data = get_market_data(condition_id)\n            if market_data:\n                for token in market_data['tokens']:\n                    if token['outcome'].lower() == side_a_outcome.lower():\n                        price = token['price']\n                        side_a_cost += price\n                        side_a_info.append((market_data['question'], side_a_outcome, price))\n\n        for condition_id in side_b_ids:\n            market_data = get_market_data(condition_id)\n            if market_data:\n                for token in market_data['tokens']:\n                    if token['outcome'].lower() == side_b_outcome.lower():\n                        price = token['price']\n                        side_b_cost += price\n                        side_b_info.append((market_data['question'], side_b_outcome, price))\n\n        total_cost = side_a_cost + side_b_cost\n        arb_percentage = (1 - total_cost) * 100\n\n        # Log the detailed information with side costs\n        print(\n            f\"Arbitrage Opportunity Found! {strategy_name} - Arbitrage Percentage: {arb_percentage}% (Side A: {side_a_cost}, Side B: {side_b_cost})\")\n\n        results.append((strategy_name, arb_percentage))\n\n    return results\n\n\ndef update_csv_file_every_minute(csv_file, arb_opportunities):\n    while True:\n        # Calculate arbitrage opportunities\n        arb_results = calculate_multiple_arbitrage_opportunities(arb_opportunities)\n\n        # Prepare the data to write to the CSV\n        timestamp = datetime.now().strftime(\"%m\/%d\/%Y %H:%M\")\n        row_data = [timestamp]\n\n        for result in arb_results:\n            strategy_name, arb_percentage = result\n            row_data.append(arb_percentage)\n\n        # Write the data to the CSV\n        try:\n            file_exists = os.path.isfile(csv_file)\n            with open(csv_file, 'a', newline='') as csvfile:\n                writer = csv.writer(csvfile)\n\n                # If file does not exist, write the header\n                if not file_exists:\n                    header = [\"Timestamp\"] + [result[0] for result in arb_results]\n                    writer.writerow(header)\n\n                writer.writerow(row_data)\n            print(f\"Data appended to {csv_file} at {timestamp}\")\n        except IOError as e:\n            print(f\"Error writing to CSV: {e}\")\n\n        # Sleep for 1 minute\n        time.sleep(60)\n\nif __name__ == \"__main__\":\n    # Fetch all markets and write to CSV\n    markets_list = fetch_all_markets(client)\n    write_markets_to_csv(markets_list)\n<\/pre>\n<!-- \/wp:enlighter\/codeblock --><\/div><\/li>\n<li><div class=\"wp-block-latest-posts__featured-image\"><img decoding=\"async\" width=\"150\" height=\"150\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/09\/article_about_degenerate_gambling_on_polymarket_com_with_a_more_dramatic_steamroller_getting_an_investor-150x150.jpeg\" class=\"attachment-thumbnail size-thumbnail wp-post-image lazyload\" alt=\"\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 150px; --smush-placeholder-aspect-ratio: 150\/150;\" \/><\/div><a class=\"wp-block-latest-posts__post-title\" href=\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/09\/05\/degenerate-gambling-on-polymarket-com-picking-up-pennies-in-front-of-a-steamroller-for-a-12-09-yield\/\">Degenerate Gambling on Polymarket.com: Picking Up Pennies in Front of a Steamroller&#8230; for a 12.09% Yield?<\/a><div class=\"wp-block-latest-posts__post-author\">by JeremyWhittaker<\/div><time datetime=\"2024-09-05T15:45:29-07:00\" class=\"wp-block-latest-posts__post-date\">September 5, 2024<\/time><div class=\"wp-block-latest-posts__post-full-content\"><!-- wp:paragraph -->\n<p>Recently, I placed a bet on Polymarket.com, speculating that Kamala Harris and JD Vance <strong>will not<\/strong> debate before the upcoming election. With just 60 days left, the odds looked favorable, and this trade seemed like an easy win. But as any seasoned investor knows, the real question is: <em>Is it worth picking up pennies in front of a steamroller?<\/em><\/p>\n<!-- \/wp:paragraph -->\n\n<div style=\"text-align: center\">\n\n<\/div>\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">My Trade Breakdown<\/h3>\n<!-- \/wp:heading -->\n\n\n<!-- wp:paragraph -->\n<p>Here\u2019s how my position stands:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list -->\n<ul class=\"wp-block-list\"><!-- wp:list-item -->\n<li><strong>Outcome<\/strong>: No (Harris and Vance will not debate)<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Quantity<\/strong>: 5,740 shares<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Average Price<\/strong>: 98.2\u00a2 per share<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Total Cost<\/strong>: \\$5,637.09<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Potential Payout<\/strong>: \\$5,740<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Potential Profit<\/strong>: \\$102.91<\/li>\n<!-- \/wp:list-item --><\/ul>\n<!-- \/wp:list -->\n\n<!-- wp:paragraph -->\n<p>The potential profit might seem modest, but when you consider the short time horizon, it\u2019s important to look deeper into the numbers.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Return on Investment (ROI) and Yield<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>Let\u2019s calculate the return:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:list {\"ordered\":true} -->\n<ol class=\"wp-block-list\"><!-- wp:list-item -->\n<li><strong>Total Payout<\/strong>: \\$5,740<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Total Cost<\/strong>: \\$5,637.09<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Profit<\/strong>: \\$102.91<\/li>\n<!-- \/wp:list-item -->\n\n<!-- wp:list-item -->\n<li><strong>Return on Investment (ROI)<\/strong>:<\/li>\n<!-- \/wp:list-item --><\/ol>\n<!-- \/wp:list -->\n\n<!-- wp:shortcode -->\n[latexpage]\n<!-- \/wp:shortcode -->\n\n\\[ \\text{ROI} = \\frac{\\text{Profit}}{\\text{Total Cost}} \\times 100 \\]\n\n\\[ \\text{ROI} = \\frac{102.91}{5637.09} \\times 100 = 1.83\\% \\]\n\n<!-- wp:paragraph -->\n<p>Although a <strong>1.83% return<\/strong> over 60 days might not sound like much, the <strong>annualized yield<\/strong> adds a different perspective. Using the formula for annualized returns:<\/p>\n<!-- \/wp:paragraph -->\n\n\\[ \\text{Annual Yield} = \\left(1 + \\frac{1.83}{100}\\right)^{\\frac{365}{60}} &#8211; 1 \\]\n\n\\[ \\text{Annual Yield} \\approx 12.09\\% \\]\n\n<!-- wp:paragraph -->\n<p>That\u2019s a <strong>12.09% annualized yield<\/strong>, which suddenly makes this trade more appealing. But there&#8217;s more to consider.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">The Risk of the Steamroller<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>This trade feels safe\u2014after all, the odds of Harris and Vance debating seem extremely low. Yet, there\u2019s always the slim possibility that something unexpected happens in the political world.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:paragraph -->\n<p>In the words of Nassim Nicholas Taleb:<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:quote -->\n<blockquote class=\"wp-block-quote\"><!-- wp:paragraph -->\n<p>&#8220;I was convinced that I was totally incompetent in predicting market prices &#8211; but that others were generally incompetent also but did not know it, or did not know they were taking massive risks. Most traders were just &#8216;picking pennies in front of a steamroller,&#8217; exposing themselves to the high-impact rare event yet sleeping like babies, unaware of it.&#8221;<\/p>\n<!-- \/wp:paragraph --><\/blockquote>\n<!-- \/wp:quote -->\n\n<!-- wp:paragraph -->\n<p>Taleb\u2019s quote resonates with this situation perfectly. It highlights the danger of taking what looks like an easy bet, without fully appreciating the rare but devastating risks lurking in the background. In my case, that steamroller is the unexpected possibility of a debate between Harris and Vance, which could be triggered by an unforeseen political event.<\/p>\n<!-- \/wp:paragraph -->\n\n<!-- wp:heading {\"level\":3} -->\n<h3 class=\"wp-block-heading\">Conclusion<\/h3>\n<!-- \/wp:heading -->\n\n<!-- wp:paragraph -->\n<p>The <strong>1.83% return<\/strong> over 60 days isn\u2019t life-changing, but with an <strong>annualized yield of 12.09%<\/strong>, this trade seems tempting. However, like Taleb warns, many traders sleep soundly while unknowingly exposing themselves to rare, high-impact risks. While I remain confident in the outcome, I can\u2019t entirely ignore the chance of an unexpected political twist that flattens this seemingly &#8220;safe&#8221; trade.<\/p>\n<!-- \/wp:paragraph -->\n<\/div><\/li>\n<\/ul>","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-654","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.2 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Jeremy Whittaker | Markets, Software, Housing, and Energy<\/title>\n<meta name=\"description\" content=\"Research, software, and practical systems across markets, Arizona housing, energy, and civic work.\" \/>\n<meta name=\"robots\" content=\"noindex, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Jeremy Whittaker | Markets, Software, Housing, and Energy\" \/>\n<meta property=\"og:description\" content=\"Research, software, and practical systems across markets, Arizona housing, energy, and civic work.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/new.jeremywhittaker.com\/\" \/>\n<meta property=\"og:site_name\" content=\"Jeremy Whittaker\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/WhittakerJeremy\" \/>\n<meta property=\"article:modified_time\" content=\"2026-03-21T19:47:38+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/new.jeremywhittaker.com\/\",\"url\":\"https:\/\/new.jeremywhittaker.com\/\",\"name\":\"Jeremy Whittaker | Markets, Software, Housing, and Energy\",\"isPartOf\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/#website\"},\"datePublished\":\"2021-09-21T22:08:11+00:00\",\"dateModified\":\"2026-03-21T19:47:38+00:00\",\"description\":\"Research, software, and practical systems across markets, Arizona housing, energy, and civic work.\",\"breadcrumb\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/new.jeremywhittaker.com\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/new.jeremywhittaker.com\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/new.jeremywhittaker.com\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Jeremy Whittaker\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/new.jeremywhittaker.com\/#website\",\"url\":\"https:\/\/new.jeremywhittaker.com\/\",\"name\":\"Jeremy Whittaker\",\"description\":\"Research, software, markets, housing, and energy\",\"publisher\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/new.jeremywhittaker.com\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c\",\"name\":\"JeremyWhittaker\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g\",\"caption\":\"JeremyWhittaker\"},\"logo\":{\"@id\":\"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g\"},\"sameAs\":[\"http:\/\/www.jeremywhittaker.com\",\"https:\/\/www.facebook.com\/WhittakerJeremy\",\"https:\/\/www.linkedin.com\/in\/jeremywhittaker\/\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Jeremy Whittaker | Markets, Software, Housing, and Energy","description":"Research, software, and practical systems across markets, Arizona housing, energy, and civic work.","robots":{"index":"noindex","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"og_locale":"en_US","og_type":"article","og_title":"Jeremy Whittaker | Markets, Software, Housing, and Energy","og_description":"Research, software, and practical systems across markets, Arizona housing, energy, and civic work.","og_url":"https:\/\/new.jeremywhittaker.com\/","og_site_name":"Jeremy Whittaker","article_publisher":"https:\/\/www.facebook.com\/WhittakerJeremy","article_modified_time":"2026-03-21T19:47:38+00:00","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/new.jeremywhittaker.com\/","url":"https:\/\/new.jeremywhittaker.com\/","name":"Jeremy Whittaker | Markets, Software, Housing, and Energy","isPartOf":{"@id":"https:\/\/new.jeremywhittaker.com\/#website"},"datePublished":"2021-09-21T22:08:11+00:00","dateModified":"2026-03-21T19:47:38+00:00","description":"Research, software, and practical systems across markets, Arizona housing, energy, and civic work.","breadcrumb":{"@id":"https:\/\/new.jeremywhittaker.com\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/new.jeremywhittaker.com\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/new.jeremywhittaker.com\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/new.jeremywhittaker.com\/"},{"@type":"ListItem","position":2,"name":"Jeremy Whittaker"}]},{"@type":"WebSite","@id":"https:\/\/new.jeremywhittaker.com\/#website","url":"https:\/\/new.jeremywhittaker.com\/","name":"Jeremy Whittaker","description":"Research, software, markets, housing, and energy","publisher":{"@id":"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/new.jeremywhittaker.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c","name":"JeremyWhittaker","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g","caption":"JeremyWhittaker"},"logo":{"@id":"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g"},"sameAs":["http:\/\/www.jeremywhittaker.com","https:\/\/www.facebook.com\/WhittakerJeremy","https:\/\/www.linkedin.com\/in\/jeremywhittaker\/"]}]}},"_links":{"self":[{"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/pages\/654","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/comments?post=654"}],"version-history":[{"count":1,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/pages\/654\/revisions"}],"predecessor-version":[{"id":12381,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/pages\/654\/revisions\/12381"}],"wp:attachment":[{"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/media?parent=654"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}