Module:BassaridMarketActivity: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
Line 1: Line 1:
---------------------------------------------------------
---------------------------------------------------------
-- Module: BassaridiaStockMarketTable
-- Module:BassaridMarketActivity (Signal-Driven)
--  
--
-- Produces a "stock table" using the same daily logic,
-- Original behavior:
-- randomization, 30% cap, and reason pools as your
-- • Daily table of investor "stock" activity with ±30% cap
-- original "BassaridiaDailyInvestorTable" script.
--  • Randomized buy/sell creating Open/Close values
--  
--  • Unique reasons pulled from increase/decrease pools
-- The key fix here is: we generate valid wikitext
--
-- rows for each entry so that the table actually shows up.
-- Change (Jan 2026):
--  • Market direction and magnitude are influenced by the same
--   operational signals used in the price modules:
--      - Weather / Ops Alert via Module:BassaridiaForecast
--      - War League tempo via Module:WarLeagueDivisionOpsBVR
--      - Hatch Ministry tempo via Module:HatchMinistryDailyOps
--      - Temple missionary tempo via Module:MissionaryDailyOpsBVR
--      - Public health via Module:BassaridiaOutbreaks
--
-- Performance:
-- • All external modules are called at most ONCE per page render.
--
-- Usage:
--   {{#invoke:BassaridMarketActivity|generateTable}}
--  Optional args:
--    |hub=Vaeringheim  (city used for national signal baselines)
--    |shareprice=30    (default 30 Polis/share)
---------------------------------------------------------
---------------------------------------------------------
local p = {}
local p = {}


---------------------------------------------------------
---------------------------------------------------------
-- 1) Date logic, same as original
-- 1) Date logic (same calendar)
---------------------------------------------------------
---------------------------------------------------------
local function getDailyDate()
local function getDailyDate()
    local startDate = os.time({year = 1999, month = 8, day = 6})
local startDate = os.time({year = 1999, month = 8, day = 6})
    local secondsInDay = 86400
local secondsInDay = 86400
    local daysPerYear = 183
local daysPerYear = 183


    local now = os.time()
local now = os.time()
    local totalDaysElapsed = math.floor((now - startDate) / secondsInDay)
local totalDaysElapsed = math.floor((now - startDate) / secondsInDay)
local yearFraction = totalDaysElapsed / daysPerYear
local psscYear = math.floor(yearFraction)
local dayOfYear = math.floor((yearFraction - psscYear) * daysPerYear) + 1


    local yearFraction = totalDaysElapsed / daysPerYear
local month, dayOfMonth
    local psscYear = math.floor(yearFraction)
if dayOfYear <= 61 then
    local dayOfYear = math.floor((yearFraction - psscYear) * daysPerYear) + 1
month = 1
dayOfMonth = dayOfYear
elseif dayOfYear <= 122 then
month = 2
dayOfMonth = dayOfYear - 61
else
month = 3
dayOfMonth = dayOfYear - 122
end


    local month, dayOfMonth
local dateString = string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear)
    if dayOfYear <= 61 then
return dateString, dayOfYear, psscYear
        month = 1
        dayOfMonth = dayOfYear
    elseif dayOfYear <= 122 then
        month = 2
        dayOfMonth = dayOfYear - 61
    else
        month = 3
        dayOfMonth = dayOfYear - 122
    end
 
    local dateString = string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear)
    return dateString, dayOfYear
end
end


---------------------------------------------------------
---------------------------------------------------------
-- 2) Data: Tickers/Companies (same stock counts)
-- 2) Data: Tickers/Companies (unchanged)
---------------------------------------------------------
---------------------------------------------------------
local companies = {
local companies = {
    {
{ flag = [[File:VaeringheimDivisionFlag.png|25px]], symbol = "VREG", name = "Vaeringheim Regional Investor (Bassaridia Vaeringheim)", stocks = 17424 },
        flag = [[File:VaeringheimDivisionFlag.png|25px]],
{ flag = [[File:AlpazkigzDivisionFlag.png|25px]], symbol = "ALPK", name = "Alpazkigz Regional Investor (Bassaridia Vaeringheim)", stocks = 16214 },
        symbol = "VREG",
{ flag = [[File:OdiferiaFlag.png|25px]], symbol = "ODFR", name = "Odiferia Regional Investor (Bassaridia Vaeringheim)", stocks = 14718 },
        name   = "Vaeringheim Regional Investor (Bassaridia Vaeringheim)",
{ flag = [[File:HafaanFlag.png|25px]], symbol = "HAFA", name = "Hafaan Regional Investor (Bassaridia Vaeringheim)", stocks = 3630 },
        stocks = 17424
{ flag = [[File:JeseriFlag.png|25px]], symbol = "JESR", name = "Jeseri Regional Investor (Bassaridia Vaeringheim)", stocks = 1870 },
    },
{ flag = [[File:HatchMinistryFlag.png|25px]], symbol = "HMIN", name = "Hatch Ministry Regional Investor (Bassaridia Vaeringheim)", stocks = 7810 },
    {
{ flag = [[File:NewSouthJangsongFlag.png|25px]], symbol = "JANG", name = "New South Jangsong Regional Investor (Bassaridia Vaeringheim)", stocks = 11085 },
        flag = [[File:AlpazkigzDivisionFlag.png|25px]],
{ flag = [[File:HaifaDivisionFlag.png|25px]], symbol = "HBAS", name = "Haifan Bassaridia Regional Investor (Bassaridia Vaeringheim)", stocks = 12500 },
        symbol = "ALPK",
{ flag = [[File:BassaridianNormarkFlagB.png|25px]], symbol = "BSNO", name = "Regional Investor of Bassaridian Normark (Bassaridia Vaeringheim)", stocks = 8852 },
        name   = "Alpazkigz Regional Investor (Bassaridia Vaeringheim)",
{ flag = [[File:Kingdom of Moorland flag.png|25px]], symbol = "SMOR", name = "Sovereign Moorland Fund (Moorland)", stocks = 10546 },
        stocks = 16214
{ symbol = "SVWF", name = "Sovereign Wealth Fund (Imperial Federation)", stocks = 827 },
    },
{ symbol = "ADRS", name = "Adrestia Wealth Fund (Imperial Federation)", stocks = 7456 },
    {
{ symbol = "HAIF", name = "Haifa Wealth Wealth Fund (Imperial Federation)", stocks = 10183 },
        flag = [[File:OdiferiaFlag.png|25px]],
{ symbol = "STRC", name = "Strait of Haifa Corporation (Imperial Federation)", stocks = 4493 },
        symbol = "ODFR",
{ symbol = "BIND", name = "Baicha Heavy Industries (Imperial Federation)", stocks = 21183 },
        name   = "Odiferia Regional Investor (Bassaridia Vaeringheim)",
{ symbol = "GFIF", name = "General Fund of the Imperial Federation (Imperial Federation)", stocks = 876 },
        stocks = 14718
{ symbol = "POCO", name = "Port of Corumia (Bassaridia Vaeringheim)", stocks = 19008 },
    },
{ symbol = "EURA", name = "Port of Euranidom (Bassaridia Vaeringheim)", stocks = 15559 },
    {
{ symbol = "POSK", name = "Port of Southern Keltiania (Bassaridia Vaeringheim)", stocks = 17101 },
        flag = [[File:HafaanFlag.png|25px]],
{ symbol = "OURN", name = "Regional Investor of the Ouriana (Bassaridia Vaeringheim)", stocks = 9757 },
        symbol = "HAFA",
        name   = "Hafaan Regional Investor (Bassaridia Vaeringheim)",
        stocks = 3630
    },
    {
        flag = [[File:JeseriFlag.png|25px]],
        symbol = "JESR",
        name   = "Jeseri Regional Investor (Bassaridia Vaeringheim)",
        stocks = 1870
    },
    {
        flag = [[File:HatchMinistryFlag.png|25px]],
        symbol = "HMIN",
        name   = "Hatch Ministry Regional Investor (Bassaridia Vaeringheim)",
        stocks = 7810
    },
    {
        flag = [[File:NewSouthJangsongFlag.png|25px]],
        symbol = "JANG",
        name   = "New South Jangsong Regional Investor (Bassaridia Vaeringheim)",
        stocks = 11085
    },
    {
        flag = [[File:HaifaDivisionFlag.png|25px]],
        symbol = "HBAS",
        name   = "Haifan Bassaridia Regional Investor (Bassaridia Vaeringheim)",
        stocks = 12500
    },
    {
        flag = [[File:BassaridianNormarkFlagB.png|25px]],
        symbol = "BSNO",
        name   = "Regional Investor of Bassaridian Normark (Bassaridia Vaeringheim)",
        stocks = 8852
    },
{
        flag = [[File:Kingdom of Moorland flag.png|25px]],
        symbol = "SMOR",
        name   = "Sovereign Moorland Fund (Moorland)",
        stocks = 10546
    },
{
        symbol = "SVWF",
        name   = "Sovereign Wealth Fund (Imperial Federation)",
        stocks = 827
    },
{
        symbol = "ADRS",
        name   = "Adrestia Wealth Fund (Imperial Federation)",
        stocks = 7456
    },
{
        symbol = "HAIF",
        name   = "Haifa Wealth Wealth Fund (Imperial Federation)",
        stocks = 10183
    },
 
{  
        symbol = "STRC",
        name   = "Strait of Haifa Corporation (Imperial Federation)",
        stocks = 4493
    },
{  
        symbol = "BIND",
        name   = "Baicha Heavy Industries (Imperial Federation)",
        stocks = 21183
    },
{  
        symbol = "GFIF",
        name   = "General Fund of the Imperial Federation (Imperial Federation)",
        stocks = 876
    },
{  
        symbol = "POCO",
        name   = "Port of Corumia (Bassaridia Vaeringheim)",
        stocks = 19008
    },
{  
        symbol = "EURA",
        name   = "Port of Euranidom (Bassaridia Vaeringheim)",
        stocks = 15559
    },
{  
        symbol = "POSK",
        name   = "Port of Southern Keltiania (Bassaridia Vaeringheim)",
        stocks = 17101
    },
{  
        symbol = "OURN",
        name   = "Regional Investor of the Ouriana (Bassaridia Vaeringheim)",
        stocks = 9757
    },
}
}


---------------------------------------------------------
---------------------------------------------------------
-- 3) Reasons: identical to original
-- 3) Reasons (unchanged)
---------------------------------------------------------
---------------------------------------------------------
local increaseReasons = {
local increaseReasons = {
    "Improved logistics at the General Port attracted new investors.",
"Improved logistics at the General Port attracted new investors.",
    "An influx of goods from Delphica boosted trade volume at the port.",
"An influx of goods from Delphica boosted trade volume at the port.",
    "High demand for Noctic-Rabrev exports raised investor confidence.",
"High demand for Noctic-Rabrev exports raised investor confidence.",
    "Completion of a new shipping lane connecting Nexa increased activity.",
"Completion of a new shipping lane connecting Nexa increased activity.",
    "Successful negotiations with Erythros merchants expanded trade.",
"Successful negotiations with Erythros merchants expanded trade.",
    "Festival of Light in Vaeringheim drove significant port activity.",
"Festival of Light in Vaeringheim drove significant port activity.",
    "High attendance at the Bassaridia Trade Symposium enhanced reputation.",
"High attendance at the Bassaridia Trade Symposium enhanced reputation.",
    "Discovery of new mineral resources near Acheron increased export potential.",
"Discovery of new mineral resources near Acheron increased export potential.",
    "A surge in ceremonial wine exports from the Blood Vineyards bolstered ops.",
"A surge in ceremonial wine exports from the Blood Vineyards bolstered ops.",
    "Efficient cargo handling improvements boosted investor sentiment.",
"Efficient cargo handling improvements boosted investor sentiment.",
    "Seasonal agricultural exports from Jogi saw a sharp increase.",
"Seasonal agricultural exports from Jogi saw a sharp increase.",
    "High sales of pearl and abalone products boosted port traffic.",
"High sales of pearl and abalone products boosted port traffic.",
    "Luxury goods from Tel-Amin attracted new high-end investors.",
"Luxury goods from Tel-Amin attracted new high-end investors.",
    "Port capacity expansion in Suncliff improved competitiveness.",
"Port capacity expansion in Suncliff improved competitiveness.",
    "Improved relations with Maskmakers Guild stabilized trade routes.",
"Improved relations with Maskmakers Guild stabilized trade routes.",
    "Increased fishing yields around Lake Morovia led to more port activity.",
"Increased fishing yields around Lake Morovia led to more port activity.",
    "Enhanced security measures at the Port reduced piracy concerns.",
"Enhanced security measures at the Port reduced piracy concerns.",
    "Cultural appreciation for Horehound exports boosted confidence.",
"Cultural appreciation for Horehound exports boosted confidence.",
    "Trade deals with Saluria expanded port usage significantly.",
"Trade deals with Saluria expanded port usage significantly.",
    "Demand for silk from the Seamstresses of Rouge improved port revenue.",
"Demand for silk from the Seamstresses of Rouge improved port revenue.",
    "Moonlit Tide celebrations increased ceremonial goods traffic.",
"Moonlit Tide celebrations increased ceremonial goods traffic.",
    "Launch of new Garganid fertilizer shipments attracted agriculture investors.",
"Launch of new Garganid fertilizer shipments attracted agriculture investors.",
    "Global interest in sacred resin exports enhanced shipments.",
"Global interest in sacred resin exports enhanced shipments.",
    "Marketing campaign highlighted the Port's strategic location.",
"Marketing campaign highlighted the Port's strategic location.",
    "Completion of high-speed rail connections boosted trade efficiency.",
"Completion of high-speed rail connections boosted trade efficiency.",
    "Launch of eco-friendly shipping initiatives attracted global partnerships.",
"Launch of eco-friendly shipping initiatives attracted global partnerships.",
    "Discovery of rare gemstones near Lake Morovia boosted export activity.",
"Discovery of rare gemstones near Lake Morovia boosted export activity.",
    "Expansion of refrigerated shipping services opened new perishable markets.",
"Expansion of refrigerated shipping services opened new perishable markets.",
    "Introduction of automation technology improved cargo processing speeds.",
"Introduction of automation technology improved cargo processing speeds.",
    "Partnerships with Aurelia’s gold traders expanded high-value exports.",
"Partnerships with Aurelia’s gold traders expanded high-value exports.",
    "Promotion of Bassaridian craftsmanship increased demand for artisan goods.",
"Promotion of Bassaridian craftsmanship increased demand for artisan goods.",
    "Hosting the International Maritime Summit enhanced port prestige.",
"Hosting the International Maritime Summit enhanced port prestige.",
    "Revival of ancient trade routes through Nexa increased traffic flow.",
"Revival of ancient trade routes through Nexa increased traffic flow.",
    "Surge in demand for Bassaridian spices and aromatics in global markets.",
"Surge in demand for Bassaridian spices and aromatics in global markets.",
    "Successful lobbying for lower tariffs improved competitiveness.",
"Successful lobbying for lower tariffs improved competitiveness.",
    "War League convoy escorts established a secure corridor through the Strait of Haifa, lowering insurance rates and drawing fresh capital.",
"War League convoy escorts established a secure corridor through the Strait of Haifa, lowering insurance rates and drawing fresh capital.",
    "Revived Stripping Path pilgrimages created steady demand for ritual goods logistics and hospitality sailings.",
"Revived Stripping Path pilgrimages created steady demand for ritual goods logistics and hospitality sailings.",
    "Record Norsolyrian wisp‑rice harvests filled new bulk carriers, lifting throughput and investor interest.",
"Record Norsolyrian wisp‑rice harvests filled new bulk carriers, lifting throughput and investor interest.",
    "The Maskmakers’ Guild Triennial Veiling drew collectors; premium cargo bookings surged.",
"The Maskmakers’ Guild Triennial Veiling drew collectors; premium cargo bookings surged.",
    "Seamstresses of Rouge hosted the Crimson Looms Exposition, spiking silk consignments and forward orders.",
"Seamstresses of Rouge hosted the Crimson Looms Exposition, spiking silk consignments and forward orders.",
    "Temple auctions of purified relics in Delphica brought in high‑value consignments and buyers.",
"Temple auctions of purified relics in Delphica brought in high‑value consignments and buyers.",
    "Thorsalon’s “Red Year” at the Blood Vineyards multiplied ceremonial wine exports and margins.",
"Thorsalon’s “Red Year” at the Blood Vineyards multiplied ceremonial wine exports and margins.",
    "Reef‑restoration around Lake Morovia improved abalone and pearl quality, boosting luxury cargoes.",
"Reef‑restoration around Lake Morovia improved abalone and pearl quality, boosting luxury cargoes.",
    "A treaty converting Ardclach corsairs into licensed privateers reduced raids and enticed risk‑averse funds.",
"A treaty converting Ardclach corsairs into licensed privateers reduced raids and enticed risk‑averse funds.",
    "An auspicious Oracle of Ephyra prophecy forecast strong trade winds, lifting sentiment across maritime markets.",
"An auspicious Oracle of Ephyra prophecy forecast strong trade winds, lifting sentiment across maritime markets.",
    "Fishery stewardship raised whitefish and sturgeon yields on Lake Morovia, sustaining cold‑chain volumes.",
"Fishery stewardship raised whitefish and sturgeon yields on Lake Morovia, sustaining cold‑chain volumes.",
    "Guild micro‑insurance pooled storm risks for small exporters, unlocking new capital inflows.",
"Guild micro‑insurance pooled storm risks for small exporters, unlocking new capital inflows.",
    "Deep‑draft dredging allowed heavier bullion and ore shipments from Aurelia to call reliably.",
"Deep‑draft dredging allowed heavier bullion and ore shipments from Aurelia to call reliably.",
    "Anti‑counterfeit seals from Nexa increased trust in sacred artifacts, strengthening high‑margin trades.",
"Anti‑counterfeit seals from Nexa increased trust in sacred artifacts, strengthening high‑margin trades.",
    "Garganid‑based soil programs lifted Jogi crop output, increasing refrigerated agri‑exports.",
"Garganid‑based soil programs lifted Jogi crop output, increasing refrigerated agri‑exports.",
    "The Festival of Red Mirth in Erythros spurred dye, spice, and ceremonial garment shipments.",
"The Festival of Red Mirth in Erythros spurred dye, spice, and ceremonial garment shipments.",
    "Acheron canyon routes reopened after landslide clearance, restoring mineral and stone exports.",
"Acheron canyon routes reopened after landslide clearance, restoring mineral and stone exports.",
    "Eco‑keel retrofits qualified fleets for green‑corridor rebates, attracting sustainability‑focused investors.",
"Eco‑keel retrofits qualified fleets for green‑corridor rebates, attracting sustainability‑focused investors.",
    "Joint littoral patrols with Sufriya and Tel‑Amin suppressed smuggling, raising throughput predictability.",
"Joint littoral patrols with Sufriya and Tel‑Amin suppressed smuggling, raising throughput predictability.",
    "Moon‑aligned schedules optimized tide windows during Moonlit Tide, cutting delays and attracting volume.",
"Moon‑aligned schedules optimized tide windows during Moonlit Tide, cutting delays and attracting volume.",
    "An artisan diaspora fair highlighted Bassaridian craft, lifting curio and high‑value small‑parcel traffic.",
"An artisan diaspora fair highlighted Bassaridian craft, lifting curio and high‑value small‑parcel traffic.",
    "Suncliff fisheries adopted by‑catch limits, enabling certification premiums on pufferfish and tuna exports.",
"Suncliff fisheries adopted by‑catch limits, enabling certification premiums on pufferfish and tuna exports.",
    "Lake Morovia ice‑pilot program extended the season, stabilizing quarterly revenues.",
"Lake Morovia ice‑pilot program extended the season, stabilizing quarterly revenues.",
    "Reopening of shrine markets in Lunalis Sancta revived the ceremonial incense trade.",
"Reopening of shrine markets in Lunalis Sancta revived the ceremonial incense trade.",
    "Aurelia’s mint issued guaranteed bullion marks, raising confidence in precious‑cargo consignments.",
"Aurelia’s mint issued guaranteed bullion marks, raising confidence in precious‑cargo consignments.",
    "New reefer rail spurs from Diamandis synchronized diamonds and perishables to maximize vessel utilization.",
"New reefer rail spurs from Diamandis synchronized diamonds and perishables to maximize vessel utilization.",
    "Academic exchange with Delphica drove books, instruments, and research equipment demand.",
"Academic exchange with Delphica drove books, instruments, and research equipment demand.",
    "Salurian co‑ops scaled ethical horehound cultivation, winning major wellness contracts.",
"Salurian co‑ops scaled ethical horehound cultivation, winning major wellness contracts.",
    "Port‑led mangrove planting improved storm resilience and brand value with ESG funds.",
"Port‑led mangrove planting improved storm resilience and brand value with ESG funds.",
    "Digital berthing and crane automation trimmed dwell times, enabling premium slot pricing.",
"Digital berthing and crane automation trimmed dwell times, enabling premium slot pricing.",
    "Harmonization with Maritime Markets guild codes simplified customs across partner ports.",
"Harmonization with Maritime Markets guild codes simplified customs across partner ports.",
    "Reformed Stripping Path processions brought orderly, revenue‑rich pilgrimage sailings.",
"Reformed Stripping Path processions brought orderly, revenue‑rich pilgrimage sailings.",
    "Sun festivals in Vaeringheim increased seasonal passenger‑freight combo charters.",
"Sun festivals in Vaeringheim increased seasonal passenger‑freight combo charters.",
    "Private equity backed a second logistics park by the General Port, expanding warehousing and value‑add services.",
"Private equity backed a second logistics park by the General Port, expanding warehousing and value‑add services.",
    "Abalone hatchery successes delivered consistent volumes for luxury food markets, lifting forward bookings."
"Abalone hatchery successes delivered consistent volumes for luxury food markets, lifting forward bookings.",
}
}
local decreaseReasons = {
local decreaseReasons = {
    "Pirate raids in the Strait of Haifa disrupted shipments to the Port.",
"Pirate raids in the Strait of Haifa disrupted shipments to the Port.",
    "Unfavorable weather delayed agriculture exports from Plains of Jogi.",
"Unfavorable weather delayed agriculture exports from Plains of Jogi.",
    "Labor shortages in the Port caused cargo-handling delays.",
"Labor shortages in the Port caused cargo-handling delays.",
    "A major fire in Salurian sacred groves reduced resin exports.",
"A major fire in Salurian sacred groves reduced resin exports.",
    "Rising costs of Noctic-Rabrev cultivation hurt confidence.",
"Rising costs of Noctic-Rabrev cultivation hurt confidence.",
    "Unrest in Jangsong Province disrupted camel meat shipments.",
"Unrest in Jangsong Province disrupted camel meat shipments.",
    "Currency fluctuations in Vaeringheim affected stability.",
"Currency fluctuations in Vaeringheim affected stability.",
    "Economic sanctions on neighbors reduced export activity.",
"Economic sanctions on neighbors reduced export activity.",
    "Decline in ceremonial artifact production lowered traffic.",
"Decline in ceremonial artifact production lowered traffic.",
    "Cultural disputes over Horehound caused export delays.",
"Cultural disputes over Horehound caused export delays.",
    "Counterfeit relics in Nexa harmed the port's reputation.",
"Counterfeit relics in Nexa harmed the port's reputation.",
    "Fishing yields were outpaced by demand, reducing shipments.",
"Fishing yields were outpaced by demand, reducing shipments.",
    "Decline in pearl quality from Anterran sources lowered interest.",
"Decline in pearl quality from Anterran sources lowered interest.",
    "Shortage of ceremonial wine from Blood Vineyards cut exports.",
"Shortage of ceremonial wine from Blood Vineyards cut exports.",
    "Unrest among Maskmaker merchants disrupted trade agreements.",
"Unrest among Maskmaker merchants disrupted trade agreements.",
    "Less demand for silk from Seamstresses of Rouge impacted flows.",
"Less demand for silk from Seamstresses of Rouge impacted flows.",
    "Collapse of trade agreement with Tel-Amin lowered volume.",
"Collapse of trade agreement with Tel-Amin lowered volume.",
    "Seasonal labor shortages shrank port shipments significantly.",
"Seasonal labor shortages shrank port shipments significantly.",
    "Disruptions in Suncliff Fisheries slowed aquatic exports.",
"Disruptions in Suncliff Fisheries slowed aquatic exports.",
    "Alternative shipping routes bypassed the Port altogether.",
"Alternative shipping routes bypassed the Port altogether.",
    "Economic downturn in Lunalis Sancta reduced investor interest.",
"Economic downturn in Lunalis Sancta reduced investor interest.",
    "Lingering piracy concerns in Haifan waters harmed confidence.",
"Lingering piracy concerns in Haifan waters harmed confidence.",
    "Failure at Bassaridia Trade Symposium lowered traffic.",
"Failure at Bassaridia Trade Symposium lowered traffic.",
    "Natural disasters near Acheron destroyed key routes.",
"Natural disasters near Acheron destroyed key routes.",
    "Sabotage of port infrastructure caused shipping delays.",
"Sabotage of port infrastructure caused shipping delays.",
    "Political unrest in Delphica disrupted key supply lines.",
"Political unrest in Delphica disrupted key supply lines.",
    "Drought in Jogi impacted seasonal agricultural yields.",
"Drought in Jogi impacted seasonal agricultural yields.",
    "Industrial disputes in Nexa delayed mineral shipments.",
"Industrial disputes in Nexa delayed mineral shipments.",
    "Spread of disease among sea cows affected meat exports.",
"Spread of disease among sea cows affected meat exports.",
    "Loss of a major trade partner due to diplomatic tensions.",
"Loss of a major trade partner due to diplomatic tensions.",
    "Contamination of sacred resin shipments led to export bans.",
"Contamination of sacred resin shipments led to export bans.",
    "Competing ports in nearby regions diverted trade flows.",
"Competing ports in nearby regions diverted trade flows.",
    "Global recession reduced overall demand for luxury goods.",
"Global recession reduced overall demand for luxury goods.",
    "Infrastructure failure in Vaeringheim caused extended delays.",
"Infrastructure failure in Vaeringheim caused extended delays.",
    "Disruption of shipping lanes due to geopolitical conflict.",
"Disruption of shipping lanes due to geopolitical conflict.",
    "Corruption scandals involving port officials reduced trust.",
"Corruption scandals involving port officials reduced trust.",
    "Collapse of the Ardclach privateer truce spiked insured losses and triggered investor pullback.",
"Collapse of the Ardclach privateer truce spiked insured losses and triggered investor pullback.",
    "Wisp‑rice blight in Norsolyra slashed staple exports, lowering throughput guidance.",
"Wisp‑rice blight in Norsolyra slashed staple exports, lowering throughput guidance.",
    "Red tide on Lake Morovia caused a sea‑cow die‑off, prompting bans on marine products.",
"Red tide on Lake Morovia caused a sea‑cow die‑off, prompting bans on marine products.",
    "Safety probes after Skýrophos pufferfish poisonings forced emergency export suspensions.",
"Safety probes after Skýrophos pufferfish poisonings forced emergency export suspensions.",
    "A Stripping Path schism led to boycotts of ritual goods, denting festival cargoes.",
"A Stripping Path schism led to boycotts of ritual goods, denting festival cargoes.",
    "A grim Oracle of Ephyra forecast famine risks, depressing forward contracts on grain.",
"A grim Oracle of Ephyra forecast famine risks, depressing forward contracts on grain.",
    "Storm‑driven silt clogged feeder canals, delaying barge traffic to the General Port.",
"Storm‑driven silt clogged feeder canals, delaying barge traffic to the General Port.",
    "Abalone parasite outbreaks cut luxury seafood exports by a third.",
"Abalone parasite outbreaks cut luxury seafood exports by a third.",
    "Temple edicts in Thorsalon restricted blood‑wine exports to key markets.",
"Temple edicts in Thorsalon restricted blood‑wine exports to key markets.",
    "The Maskmakers’ Veiling was postponed amid scandal, shrinking high‑margin artisan flows.",
"The Maskmakers’ Veiling was postponed amid scandal, shrinking high‑margin artisan flows.",
    "A strike at the Seamstresses of Rouge halted silk output for a fortnight.",
"A strike at the Seamstresses of Rouge halted silk output for a fortnight.",
    "A port cyber outage stalled automated gates, increasing vessel idle time.",
"A port cyber outage stalled automated gates, increasing vessel idle time.",
    "Arson at a shrine‑warehouse destroyed incense inventories bound for Lunalis Sancta.",
"Arson at a shrine‑warehouse destroyed incense inventories bound for Lunalis Sancta.",
    "A counterfeit bullion scandal tied to Aurelia undermined trust in precious‑cargo manifests.",
"A counterfeit bullion scandal tied to Aurelia undermined trust in precious‑cargo manifests.",
    "Ice storms on Lake Morovia closed passages, canceling sailings for a week.",
"Ice storms on Lake Morovia closed passages, canceling sailings for a week.",
    "War League redeployments thinned convoy escorts, raising risk premia on all lanes.",
"War League redeployments thinned convoy escorts, raising risk premia on all lanes.",
    "Migratory herring shifted offshore, lowering small‑boat landings and cold‑chain loads.",
"Migratory herring shifted offshore, lowering small‑boat landings and cold‑chain loads.",
    "A jellyfish bloom fouled seawater intakes for reefers, limiting perishables capacity.",
"A jellyfish bloom fouled seawater intakes for reefers, limiting perishables capacity.",
    "A cultural boycott of horehound remedies cut Salurian exports in northern markets.",
"A cultural boycott of horehound remedies cut Salurian exports in northern markets.",
    "High holy day closures exceeded forecasts, compressing berth availability into fewer windows.",
"High holy day closures exceeded forecasts, compressing berth availability into fewer windows.",
    "Smuggler drone swarms jammed AIS beacons, triggering precautionary shutdowns.",
"Smuggler drone swarms jammed AIS beacons, triggering precautionary shutdowns.",
    "Earth tremors near Vaeringheim damaged quay cranes, forcing partial closures.",
"Earth tremors near Vaeringheim damaged quay cranes, forcing partial closures.",
    "Persistent drought in Jogi curtailed irrigation despite Garganid composting gains.",
"Persistent drought in Jogi curtailed irrigation despite Garganid composting gains.",
    "A maritime insurance strike paused new binders for Haifan waters.",
"A maritime insurance strike paused new binders for Haifan waters.",
    "Acheron rockslides again blocked canyon routes, stranding mineral consignments.",
"Acheron rockslides again blocked canyon routes, stranding mineral consignments.",
    "Pearl farm disease in coastal coves reduced gem‑quality output.",
"Pearl farm disease in coastal coves reduced gem‑quality output.",
    "Sanctions on a partner city choked off intermediate‑goods flows.",
"Sanctions on a partner city choked off intermediate‑goods flows.",
    "A relic‑authentication scandal in Nexa slashed demand for sacred artifacts.",
"A relic‑authentication scandal in Nexa slashed demand for sacred artifacts.",
    "Fuel surtaxes on heavy fuel oil rerouted liners to competing ports.",
"Fuel surtaxes on heavy fuel oil rerouted liners to competing ports.",
    "Anxiety clusters among dockworkers after nightmare seasons increased absenteeism.",
"Anxiety clusters among dockworkers after nightmare seasons increased absenteeism.",
    "Naval exercises in the Strait of Haifa imposed no‑go zones, lengthening voyages.",
"Naval exercises in the Strait of Haifa imposed no‑go zones, lengthening voyages.",
    "Eel spawning closures on feeder rivers restricted barge transits at night.",
"Eel spawning closures on feeder rivers restricted barge transits at night.",
    "Rogue privateers levied shadow tolls on coastal routes, deterring shippers.",
"Rogue privateers levied shadow tolls on coastal routes, deterring shippers.",
    "A warehouse fire in the incense quarter contaminated adjacent cargoes.",
"A warehouse fire in the incense quarter contaminated adjacent cargoes.",
    "A court injunction over contested wreck salvage tied up berths for days.",
"A court injunction over contested wreck salvage tied up berths for days.",
    "Unexpected monsoon surges sank two coastal lighters, scaring lenders and reinsurers."
"Unexpected monsoon surges sank two coastal lighters, scaring lenders and reinsurers.",
}
}
local function popUniqueReason(list)
if #list == 0 then
return "(No reason: out of reasons!)"
end
local i = math.random(#list)
local r = list[i]
table.remove(list, i)
return r
end
---------------------------------------------------------
-- Safe require (cached)
---------------------------------------------------------
local _req = {}
local function safeRequire(name)
if _req[name] ~= nil then return _req[name] end
local ok, mod = pcall(require, name)
if not ok then mod = false end
_req[name] = mod
return mod
end


---------------------------------------------------------
---------------------------------------------------------
-- 4) Utility: pop a unique reason
-- Signal extraction (once per render)
---------------------------------------------------------
---------------------------------------------------------
local function popUniqueReason(list)
local function parseOutbreakSeverity(outText)
    if #list == 0 then
outText = tostring(outText or "")
        return "(No reason: out of reasons!)"
local sev = outText:match('style="background:[^"]+"%s*|%s*([A-Z]+)%s*||')
    end
if sev == "CRITICAL" or sev == "HIGH" or sev == "MEDIUM" or sev == "LOW" or sev == "NONE" then
    local i = math.random(#list)
return sev
    local r = list[i]
end
    table.remove(list, i)
return "NONE"
    return r
end
 
local function severityToRisk(sev)
if sev == "LOW" then return 0.20 end
if sev == "MEDIUM" then return 0.45 end
if sev == "HIGH" then return 0.70 end
if sev == "CRITICAL" then return 0.95 end
return 0.0
end
 
local function opsLevelToRisk(level)
level = tonumber(level) or 0
if level <= 0 then return 0.0 end
if level == 1 then return 0.25 end
if level == 2 then return 0.55 end
return 0.85
end
 
local function clamp(x, lo, hi)
if x < lo then return lo end
if x > hi then return hi end
return x
end
 
local function computeSignals(hubCity, psscYear, dayOfYear)
local sig = {
hub = hubCity,
wxRisk = 0.0,
wxLabel = "Green",
wxCond = "",
warRisk = 0.0,
hatchIntensity = 0.0,
templeRedRate = 0.0,
healthRisk = 0.0,
mood = 0.0,
vol = 0.0,
}
 
-- Weather / Ops
local forecast = safeRequire("Module:BassaridiaForecast")
if forecast and type(forecast.getOpsData) == "function" then
local op = forecast.getOpsData(hubCity, psscYear, dayOfYear)
if op and op.alert then
sig.wxLabel = tostring(op.alert.label or "Green")
sig.wxCond  = tostring(op.condition or "")
sig.wxRisk  = opsLevelToRisk(op.alert.level)
local imp = op.impacts or {}
if tostring(imp.ground or "") == "Closed" then sig.wxRisk = sig.wxRisk + 0.10 end
if tostring(imp.sea or "") == "Closed" then sig.wxRisk = sig.wxRisk + 0.10 end
if tostring(imp.air or "") == "No-Go" then sig.wxRisk = sig.wxRisk + 0.05 end
sig.wxRisk = clamp(sig.wxRisk, 0.0, 1.0)
end
end
 
-- War League tempo
local wl = safeRequire("Module:WarLeagueDivisionOpsBVR")
if wl and type(wl.makeTable) == "function" then
local out = tostring(wl.makeTable({ args = { rows = "48", showlegend = "no", seed = tostring(psscYear * 1000 + dayOfYear) } }) or "")
local rows, high, critical = 0, 0, 0
for sev in out:gmatch("||%s*(LOW|MEDIUM|HIGH|CRITICAL)%s*||") do
rows = rows + 1
if sev == "HIGH" then high = high + 1 end
if sev == "CRITICAL" then critical = critical + 1 end
end
if rows > 0 then
local score = (high + 2 * critical) / rows
sig.warRisk = clamp(score / 2, 0.0, 1.0)
end
end
 
-- Hatch Ministry tempo
local hatch = safeRequire("Module:HatchMinistryDailyOps")
if hatch and type(hatch.dailyReport) == "function" then
local rep = tostring(hatch.dailyReport({ args = { year = tostring(psscYear), day = tostring(dayOfYear), legend = "no" } }) or "")
local k = 0
for _ in rep:gmatch("[Ii]nterdict") do k = k + 1 end
for _ in rep:gmatch("[Ii]nspect") do k = k + 1 end
for _ in rep:gmatch("[Ss]eiz") do k = k + 1 end
for _ in rep:gmatch("blockade") do k = k + 1 end
sig.hatchIntensity = clamp(k / 40, 0.0, 1.0)
end
 
-- Temple missionary tempo
local miss = safeRequire("Module:MissionaryDailyOpsBVR")
if miss and type(miss.makeTable) == "function" then
local out = tostring(miss.makeTable({ args = { region = "Core", includeThreat = "no", showEcon = "no", limit = "40", showlegend = "no" } }) or "")
local red, n = 0, 0
for sev in out:gmatch("||%s*(Green|Yellow|Red)%s*$") do
n = n + 1
if sev == "Red" then red = red + 1 end
end
if n > 0 then sig.templeRedRate = clamp(red / n, 0.0, 1.0) end
end
 
-- Public health
local outb = safeRequire("Module:BassaridiaOutbreaks")
if outb and type(outb.outbreaksForCity) == "function" then
local txt = outb.outbreaksForCity({ args = { city = hubCity } })
local sev = parseOutbreakSeverity(txt)
sig.healthRisk = severityToRisk(sev)
end
 
-- Mood [-1,1]
local mood = 0.0
mood = mood - (sig.wxRisk * 0.35)
mood = mood - (sig.warRisk * 0.30)
mood = mood - (sig.healthRisk * 0.30)
mood = mood + (sig.templeRedRate * 0.12)
mood = mood - (sig.hatchIntensity * 0.05)
sig.mood = clamp(mood, -1.0, 1.0)
 
-- Volatility [0,1]
local vol = 0.0
vol = vol + sig.wxRisk * 0.40
vol = vol + sig.warRisk * 0.35
vol = vol + sig.hatchIntensity * 0.20
vol = vol + sig.healthRisk * 0.35
sig.vol = clamp(vol, 0.0, 1.0)
 
return sig
end
end


---------------------------------------------------------
---------------------------------------------------------
-- 5) Half up, half down
-- Company-specific sensitivity (small, bounded)
---------------------------------------------------------
---------------------------------------------------------
local function assignUpDown(count)
local function companyBias(co, sig)
    local half = math.floor(count / 2)
local sym = tostring(co.symbol or "")
    local arr = {}
local name = tostring(co.name or ""):lower()
    for i = 1, half do
 
        arr[#arr+1] = "Inc"
if sym == "HMIN" or name:find("hatch ministry", 1, true) then
    end
return clamp((sig.hatchIntensity * 0.10) + (sig.warRisk * 0.06) - (sig.wxRisk * 0.04) - (sig.healthRisk * 0.04), -0.20, 0.20)
    for i = half+1, count do
end
        arr[#arr+1] = "Dec"
 
    end
if sym == "SMOR" or name:find("moorland", 1, true) then
    -- shuffle
return clamp(-(sig.warRisk * 0.08) - (sig.hatchIntensity * 0.06) - (sig.healthRisk * 0.05), -0.20, 0.20)
    for i = #arr, 2, -1 do
end
        local j = math.random(i)
 
        arr[i], arr[j] = arr[j], arr[i]
if name:find("port of", 1, true) or sym == "STRC" then
    end
return clamp(-(sig.wxRisk * 0.08) - (sig.healthRisk * 0.04) + (sig.templeRedRate * 0.03), -0.20, 0.20)
    return arr
end
 
return clamp((sig.templeRedRate * 0.02) - (sig.wxRisk * 0.03) - (sig.warRisk * 0.02) - (sig.healthRisk * 0.02), -0.12, 0.12)
end
 
local function companyVolScale(co)
local name = tostring(co.name or ""):lower()
if name:find("port of", 1, true) then return 1.15 end
if name:find("fund", 1, true) then return 0.90 end
return 1.0
end
 
local function signalPrefix(direction, co, sig)
local parts = {}
if sig.wxRisk >= 0.60 then parts[#parts+1] = "Weather/ops constraints" end
if sig.warRisk >= 0.55 then parts[#parts+1] = "War League posture" end
if sig.healthRisk >= 0.55 then parts[#parts+1] = "Public health pressure" end
if sig.hatchIntensity >= 0.55 then parts[#parts+1] = "Hatch enforcement tempo" end
if sig.templeRedRate >= 0.35 then parts[#parts+1] = "Temple mission tempo" end
 
if (co.symbol == "HMIN" or tostring(co.name or ""):lower():find("hatch ministry", 1, true)) and sig.hatchIntensity >= 0.40 then
parts[#parts+1] = "Enforcement rents"
end
 
if #parts == 0 then
return "Signals: routine conditions."
end
return "Signals: " .. table.concat(parts, "; ") .. "."
end
end


---------------------------------------------------------
---------------------------------------------------------
-- 6) Main function: produce the table
-- Main function: produce the table (signal-driven)
---------------------------------------------------------
---------------------------------------------------------
function p.generateTable(frame)
function p.generateTable(frame)
    -- Seed
local args = (frame and frame.args) or {}
    local dateString, dayOfYear = getDailyDate()
local hub = tostring(args.hub or "Vaeringheim")
    local localTime = os.date("*t")
local sharePrice = tonumber(args.shareprice or 30) or 30
    local dailySeed = localTime.year * 1000 + localTime.yday
    math.randomseed(dailySeed)
 
    -- Copy reasons
    local incPool = {}
    for _, r in ipairs(increaseReasons) do incPool[#incPool+1] = r end
    local decPool = {}
    for _, r in ipairs(decreaseReasons) do decPool[#decPool+1] = r end


    local n = #companies
local dateString, dayOfYear, psscYear = getDailyDate()
    local directions = assignUpDown(n)
local localTime = os.date("*t")
local dailySeed = localTime.year * 1000 + localTime.yday
math.randomseed(dailySeed)


    -- In the original script, each share was valued at 30 Polis
-- Copy reasons
    local sharePrice = 30
local incPool, decPool = {}, {}
for _, r in ipairs(increaseReasons) do incPool[#incPool+1] = r end
for _, r in ipairs(decreaseReasons) do decPool[#decPool+1] = r end


    -- Start building the table output
-- Signals once
    local out = {}
local sig = computeSignals(hub, psscYear, dayOfYear)
    table.insert(out, '{| class="wikitable" style="text-align:center; width:100%;"')
    -- Header row (one line, with !! for separate columns)
    table.insert(out, '|-')
    table.insert(out, '! Date !! Symbol !! Company !! Open (Polis) !! Close (Polis) !! Volume !! Change !! Change % !! Reason')


    -- Generate each row
local out = {}
    for i, co in ipairs(companies) do
table.insert(out, '{| class="wikitable" style="text-align:center; width:100%;"')
        local direction = directions[i]
table.insert(out, '|-')
        local stocksStart = co.stocks
table.insert(out, '! Date !! Symbol !! Company !! Open (Polis) !! Close (Polis) !! Volume !! Change !! Change % !! Reason')


        local maxDelta = math.floor(stocksStart * 0.3)
for _, co in ipairs(companies) do
        if maxDelta < 1 then maxDelta = 1 end
local stocksStart = tonumber(co.stocks) or 1
if stocksStart < 1 then stocksStart = 1 end


        local buy, sell = 0, 0
local bias = companyBias(co, sig)
        if direction == "Inc" then
local pInc = clamp(0.50 + (sig.mood * 0.18) + bias, 0.12, 0.88)
            buy  = math.random(1, maxDelta)
local direction = (math.random() < pInc) and "Inc" or "Dec"
            sell = math.random(0, buy - 1)
        else
            sell = math.random(1, maxDelta)
            buy  = math.random(0, sell - 1)
        end


        local stocksEnd = stocksStart + buy - sell
local volScale = companyVolScale(co)
local volFactor = clamp(0.55 + sig.vol * 0.90 * volScale, 0.45, 1.30)
local maxDelta = math.floor(stocksStart * 0.30 * volFactor)
if maxDelta < 1 then maxDelta = 1 end


        -- Convert to Polis
local buy, sell = 0, 0
        local openVal = stocksStart * sharePrice
if direction == "Inc" then
        local closeVal = stocksEnd  * sharePrice
buy  = math.random(1, maxDelta)
sell = math.random(0, buy - 1)
else
sell = math.random(1, maxDelta)
buy = math.random(0, sell - 1)
end


        -- Volume => total shares traded
local stocksEnd = stocksStart + buy - sell
        local volume = buy + sell


        -- Daily change (Polis)
local openVal  = stocksStart * sharePrice
        local changeVal = closeVal - openVal
local closeVal = stocksEnd  * sharePrice
local volume = buy + sell
local changeVal = closeVal - openVal


        -- Change %
local pctChange = 0
        local pctChange = 0
if openVal > 0 then
        if openVal > 0 then
pctChange = (changeVal / openVal) * 100
            pctChange = (changeVal / openVal) * 100
end
        end


        -- Reason
local baseReason
        local reason
if changeVal > 0 then
        if changeVal > 0 then
baseReason = popUniqueReason(incPool)
            reason = popUniqueReason(incPool)
elseif changeVal < 0 then
        elseif changeVal < 0 then
baseReason = popUniqueReason(decPool)
            reason = popUniqueReason(decPool)
else
        else
baseReason = popUniqueReason(incPool)
            -- if no net difference, pick from inc pool
end
            reason = popUniqueReason(incPool)
        end


        -- Build one row (cells separated by "||")
local reason = signalPrefix(direction, co, sig) .. " " .. baseReason
        local row = table.concat({
            '|-',
            string.format('| %s || %s || %s || %d || %d || %d || %+d || %.2f%% || %s',
                dateString,        -- e.g. "22/01/2025 PSSC"
                co.symbol,
                co.name,
                openVal,
                closeVal,
                volume,
                changeVal,
                pctChange,
                reason
            )
        }, '\n')  -- newline between |- and the row data


        table.insert(out, row)
table.insert(out, table.concat({
    end
'|-',
string.format('| %s || %s || %s || %d || %d || %d || %+d || %.2f%% || %s',
dateString,
co.symbol,
co.name,
openVal,
closeVal,
volume,
changeVal,
pctChange,
reason
)
}, '\n'))
end


    table.insert(out, '|}')
table.insert(out, '|}')
    return table.concat(out, '\n')
return table.concat(out, '\n')
end
end


return p
return p

Revision as of 03:02, 21 January 2026

Documentation for this module may be created at Module:BassaridMarketActivity/doc

---------------------------------------------------------
-- Module:BassaridMarketActivity (Signal-Driven)
--
-- Original behavior:
--  • Daily table of investor "stock" activity with ±30% cap
--  • Randomized buy/sell creating Open/Close values
--  • Unique reasons pulled from increase/decrease pools
--
-- Change (Jan 2026):
--  • Market direction and magnitude are influenced by the same
--    operational signals used in the price modules:
--      - Weather / Ops Alert via Module:BassaridiaForecast
--      - War League tempo via Module:WarLeagueDivisionOpsBVR
--      - Hatch Ministry tempo via Module:HatchMinistryDailyOps
--      - Temple missionary tempo via Module:MissionaryDailyOpsBVR
--      - Public health via Module:BassaridiaOutbreaks
--
-- Performance:
--  • All external modules are called at most ONCE per page render.
--
-- Usage:
--   {{#invoke:BassaridMarketActivity|generateTable}}
--   Optional args:
--     |hub=Vaeringheim   (city used for national signal baselines)
--     |shareprice=30     (default 30 Polis/share)
---------------------------------------------------------
local p = {}

---------------------------------------------------------
-- 1) Date logic (same calendar)
---------------------------------------------------------
local function getDailyDate()
	local startDate = os.time({year = 1999, month = 8, day = 6})
	local secondsInDay = 86400
	local daysPerYear = 183

	local now = os.time()
	local totalDaysElapsed = math.floor((now - startDate) / secondsInDay)
	local yearFraction = totalDaysElapsed / daysPerYear
	local psscYear = math.floor(yearFraction)
	local dayOfYear = math.floor((yearFraction - psscYear) * daysPerYear) + 1

	local month, dayOfMonth
	if dayOfYear <= 61 then
		month = 1
		dayOfMonth = dayOfYear
	elseif dayOfYear <= 122 then
		month = 2
		dayOfMonth = dayOfYear - 61
	else
		month = 3
		dayOfMonth = dayOfYear - 122
	end

	local dateString = string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear)
	return dateString, dayOfYear, psscYear
end

---------------------------------------------------------
-- 2) Data: Tickers/Companies (unchanged)
---------------------------------------------------------
local companies = {
	{ flag = [[File:VaeringheimDivisionFlag.png|25px]], symbol = "VREG", name = "Vaeringheim Regional Investor (Bassaridia Vaeringheim)", stocks = 17424 },
	{ flag = [[File:AlpazkigzDivisionFlag.png|25px]], symbol = "ALPK", name = "Alpazkigz Regional Investor (Bassaridia Vaeringheim)", stocks = 16214 },
	{ flag = [[File:OdiferiaFlag.png|25px]], symbol = "ODFR", name = "Odiferia Regional Investor (Bassaridia Vaeringheim)", stocks = 14718 },
	{ flag = [[File:HafaanFlag.png|25px]], symbol = "HAFA", name = "Hafaan Regional Investor (Bassaridia Vaeringheim)", stocks = 3630 },
	{ flag = [[File:JeseriFlag.png|25px]], symbol = "JESR", name = "Jeseri Regional Investor (Bassaridia Vaeringheim)", stocks = 1870 },
	{ flag = [[File:HatchMinistryFlag.png|25px]], symbol = "HMIN", name = "Hatch Ministry Regional Investor (Bassaridia Vaeringheim)", stocks = 7810 },
	{ flag = [[File:NewSouthJangsongFlag.png|25px]], symbol = "JANG", name = "New South Jangsong Regional Investor (Bassaridia Vaeringheim)", stocks = 11085 },
	{ flag = [[File:HaifaDivisionFlag.png|25px]], symbol = "HBAS", name = "Haifan Bassaridia Regional Investor (Bassaridia Vaeringheim)", stocks = 12500 },
	{ flag = [[File:BassaridianNormarkFlagB.png|25px]], symbol = "BSNO", name = "Regional Investor of Bassaridian Normark (Bassaridia Vaeringheim)", stocks = 8852 },
	{ flag = [[File:Kingdom of Moorland flag.png|25px]], symbol = "SMOR", name = "Sovereign Moorland Fund (Moorland)", stocks = 10546 },
	{ symbol = "SVWF", name = "Sovereign Wealth Fund (Imperial Federation)", stocks = 827 },
	{ symbol = "ADRS", name = "Adrestia Wealth Fund (Imperial Federation)", stocks = 7456 },
	{ symbol = "HAIF", name = "Haifa Wealth Wealth Fund (Imperial Federation)", stocks = 10183 },
	{ symbol = "STRC", name = "Strait of Haifa Corporation (Imperial Federation)", stocks = 4493 },
	{ symbol = "BIND", name = "Baicha Heavy Industries (Imperial Federation)", stocks = 21183 },
	{ symbol = "GFIF", name = "General Fund of the Imperial Federation (Imperial Federation)", stocks = 876 },
	{ symbol = "POCO", name = "Port of Corumia (Bassaridia Vaeringheim)", stocks = 19008 },
	{ symbol = "EURA", name = "Port of Euranidom (Bassaridia Vaeringheim)", stocks = 15559 },
	{ symbol = "POSK", name = "Port of Southern Keltiania (Bassaridia Vaeringheim)", stocks = 17101 },
	{ symbol = "OURN", name = "Regional Investor of the Ouriana (Bassaridia Vaeringheim)", stocks = 9757 },
}

---------------------------------------------------------
-- 3) Reasons (unchanged)
---------------------------------------------------------
local increaseReasons = {
	"Improved logistics at the General Port attracted new investors.",
	"An influx of goods from Delphica boosted trade volume at the port.",
	"High demand for Noctic-Rabrev exports raised investor confidence.",
	"Completion of a new shipping lane connecting Nexa increased activity.",
	"Successful negotiations with Erythros merchants expanded trade.",
	"Festival of Light in Vaeringheim drove significant port activity.",
	"High attendance at the Bassaridia Trade Symposium enhanced reputation.",
	"Discovery of new mineral resources near Acheron increased export potential.",
	"A surge in ceremonial wine exports from the Blood Vineyards bolstered ops.",
	"Efficient cargo handling improvements boosted investor sentiment.",
	"Seasonal agricultural exports from Jogi saw a sharp increase.",
	"High sales of pearl and abalone products boosted port traffic.",
	"Luxury goods from Tel-Amin attracted new high-end investors.",
	"Port capacity expansion in Suncliff improved competitiveness.",
	"Improved relations with Maskmakers Guild stabilized trade routes.",
	"Increased fishing yields around Lake Morovia led to more port activity.",
	"Enhanced security measures at the Port reduced piracy concerns.",
	"Cultural appreciation for Horehound exports boosted confidence.",
	"Trade deals with Saluria expanded port usage significantly.",
	"Demand for silk from the Seamstresses of Rouge improved port revenue.",
	"Moonlit Tide celebrations increased ceremonial goods traffic.",
	"Launch of new Garganid fertilizer shipments attracted agriculture investors.",
	"Global interest in sacred resin exports enhanced shipments.",
	"Marketing campaign highlighted the Port's strategic location.",
	"Completion of high-speed rail connections boosted trade efficiency.",
	"Launch of eco-friendly shipping initiatives attracted global partnerships.",
	"Discovery of rare gemstones near Lake Morovia boosted export activity.",
	"Expansion of refrigerated shipping services opened new perishable markets.",
	"Introduction of automation technology improved cargo processing speeds.",
	"Partnerships with Aurelia’s gold traders expanded high-value exports.",
	"Promotion of Bassaridian craftsmanship increased demand for artisan goods.",
	"Hosting the International Maritime Summit enhanced port prestige.",
	"Revival of ancient trade routes through Nexa increased traffic flow.",
	"Surge in demand for Bassaridian spices and aromatics in global markets.",
	"Successful lobbying for lower tariffs improved competitiveness.",
	"War League convoy escorts established a secure corridor through the Strait of Haifa, lowering insurance rates and drawing fresh capital.",
	"Revived Stripping Path pilgrimages created steady demand for ritual goods logistics and hospitality sailings.",
	"Record Norsolyrian wisp‑rice harvests filled new bulk carriers, lifting throughput and investor interest.",
	"The Maskmakers’ Guild Triennial Veiling drew collectors; premium cargo bookings surged.",
	"Seamstresses of Rouge hosted the Crimson Looms Exposition, spiking silk consignments and forward orders.",
	"Temple auctions of purified relics in Delphica brought in high‑value consignments and buyers.",
	"Thorsalon’s “Red Year” at the Blood Vineyards multiplied ceremonial wine exports and margins.",
	"Reef‑restoration around Lake Morovia improved abalone and pearl quality, boosting luxury cargoes.",
	"A treaty converting Ardclach corsairs into licensed privateers reduced raids and enticed risk‑averse funds.",
	"An auspicious Oracle of Ephyra prophecy forecast strong trade winds, lifting sentiment across maritime markets.",
	"Fishery stewardship raised whitefish and sturgeon yields on Lake Morovia, sustaining cold‑chain volumes.",
	"Guild micro‑insurance pooled storm risks for small exporters, unlocking new capital inflows.",
	"Deep‑draft dredging allowed heavier bullion and ore shipments from Aurelia to call reliably.",
	"Anti‑counterfeit seals from Nexa increased trust in sacred artifacts, strengthening high‑margin trades.",
	"Garganid‑based soil programs lifted Jogi crop output, increasing refrigerated agri‑exports.",
	"The Festival of Red Mirth in Erythros spurred dye, spice, and ceremonial garment shipments.",
	"Acheron canyon routes reopened after landslide clearance, restoring mineral and stone exports.",
	"Eco‑keel retrofits qualified fleets for green‑corridor rebates, attracting sustainability‑focused investors.",
	"Joint littoral patrols with Sufriya and Tel‑Amin suppressed smuggling, raising throughput predictability.",
	"Moon‑aligned schedules optimized tide windows during Moonlit Tide, cutting delays and attracting volume.",
	"An artisan diaspora fair highlighted Bassaridian craft, lifting curio and high‑value small‑parcel traffic.",
	"Suncliff fisheries adopted by‑catch limits, enabling certification premiums on pufferfish and tuna exports.",
	"Lake Morovia ice‑pilot program extended the season, stabilizing quarterly revenues.",
	"Reopening of shrine markets in Lunalis Sancta revived the ceremonial incense trade.",
	"Aurelia’s mint issued guaranteed bullion marks, raising confidence in precious‑cargo consignments.",
	"New reefer rail spurs from Diamandis synchronized diamonds and perishables to maximize vessel utilization.",
	"Academic exchange with Delphica drove books, instruments, and research equipment demand.",
	"Salurian co‑ops scaled ethical horehound cultivation, winning major wellness contracts.",
	"Port‑led mangrove planting improved storm resilience and brand value with ESG funds.",
	"Digital berthing and crane automation trimmed dwell times, enabling premium slot pricing.",
	"Harmonization with Maritime Markets guild codes simplified customs across partner ports.",
	"Reformed Stripping Path processions brought orderly, revenue‑rich pilgrimage sailings.",
	"Sun festivals in Vaeringheim increased seasonal passenger‑freight combo charters.",
	"Private equity backed a second logistics park by the General Port, expanding warehousing and value‑add services.",
	"Abalone hatchery successes delivered consistent volumes for luxury food markets, lifting forward bookings.",
}
local decreaseReasons = {
	"Pirate raids in the Strait of Haifa disrupted shipments to the Port.",
	"Unfavorable weather delayed agriculture exports from Plains of Jogi.",
	"Labor shortages in the Port caused cargo-handling delays.",
	"A major fire in Salurian sacred groves reduced resin exports.",
	"Rising costs of Noctic-Rabrev cultivation hurt confidence.",
	"Unrest in Jangsong Province disrupted camel meat shipments.",
	"Currency fluctuations in Vaeringheim affected stability.",
	"Economic sanctions on neighbors reduced export activity.",
	"Decline in ceremonial artifact production lowered traffic.",
	"Cultural disputes over Horehound caused export delays.",
	"Counterfeit relics in Nexa harmed the port's reputation.",
	"Fishing yields were outpaced by demand, reducing shipments.",
	"Decline in pearl quality from Anterran sources lowered interest.",
	"Shortage of ceremonial wine from Blood Vineyards cut exports.",
	"Unrest among Maskmaker merchants disrupted trade agreements.",
	"Less demand for silk from Seamstresses of Rouge impacted flows.",
	"Collapse of trade agreement with Tel-Amin lowered volume.",
	"Seasonal labor shortages shrank port shipments significantly.",
	"Disruptions in Suncliff Fisheries slowed aquatic exports.",
	"Alternative shipping routes bypassed the Port altogether.",
	"Economic downturn in Lunalis Sancta reduced investor interest.",
	"Lingering piracy concerns in Haifan waters harmed confidence.",
	"Failure at Bassaridia Trade Symposium lowered traffic.",
	"Natural disasters near Acheron destroyed key routes.",
	"Sabotage of port infrastructure caused shipping delays.",
	"Political unrest in Delphica disrupted key supply lines.",
	"Drought in Jogi impacted seasonal agricultural yields.",
	"Industrial disputes in Nexa delayed mineral shipments.",
	"Spread of disease among sea cows affected meat exports.",
	"Loss of a major trade partner due to diplomatic tensions.",
	"Contamination of sacred resin shipments led to export bans.",
	"Competing ports in nearby regions diverted trade flows.",
	"Global recession reduced overall demand for luxury goods.",
	"Infrastructure failure in Vaeringheim caused extended delays.",
	"Disruption of shipping lanes due to geopolitical conflict.",
	"Corruption scandals involving port officials reduced trust.",
	"Collapse of the Ardclach privateer truce spiked insured losses and triggered investor pullback.",
	"Wisp‑rice blight in Norsolyra slashed staple exports, lowering throughput guidance.",
	"Red tide on Lake Morovia caused a sea‑cow die‑off, prompting bans on marine products.",
	"Safety probes after Skýrophos pufferfish poisonings forced emergency export suspensions.",
	"A Stripping Path schism led to boycotts of ritual goods, denting festival cargoes.",
	"A grim Oracle of Ephyra forecast famine risks, depressing forward contracts on grain.",
	"Storm‑driven silt clogged feeder canals, delaying barge traffic to the General Port.",
	"Abalone parasite outbreaks cut luxury seafood exports by a third.",
	"Temple edicts in Thorsalon restricted blood‑wine exports to key markets.",
	"The Maskmakers’ Veiling was postponed amid scandal, shrinking high‑margin artisan flows.",
	"A strike at the Seamstresses of Rouge halted silk output for a fortnight.",
	"A port cyber outage stalled automated gates, increasing vessel idle time.",
	"Arson at a shrine‑warehouse destroyed incense inventories bound for Lunalis Sancta.",
	"A counterfeit bullion scandal tied to Aurelia undermined trust in precious‑cargo manifests.",
	"Ice storms on Lake Morovia closed passages, canceling sailings for a week.",
	"War League redeployments thinned convoy escorts, raising risk premia on all lanes.",
	"Migratory herring shifted offshore, lowering small‑boat landings and cold‑chain loads.",
	"A jellyfish bloom fouled seawater intakes for reefers, limiting perishables capacity.",
	"A cultural boycott of horehound remedies cut Salurian exports in northern markets.",
	"High holy day closures exceeded forecasts, compressing berth availability into fewer windows.",
	"Smuggler drone swarms jammed AIS beacons, triggering precautionary shutdowns.",
	"Earth tremors near Vaeringheim damaged quay cranes, forcing partial closures.",
	"Persistent drought in Jogi curtailed irrigation despite Garganid composting gains.",
	"A maritime insurance strike paused new binders for Haifan waters.",
	"Acheron rockslides again blocked canyon routes, stranding mineral consignments.",
	"Pearl farm disease in coastal coves reduced gem‑quality output.",
	"Sanctions on a partner city choked off intermediate‑goods flows.",
	"A relic‑authentication scandal in Nexa slashed demand for sacred artifacts.",
	"Fuel surtaxes on heavy fuel oil rerouted liners to competing ports.",
	"Anxiety clusters among dockworkers after nightmare seasons increased absenteeism.",
	"Naval exercises in the Strait of Haifa imposed no‑go zones, lengthening voyages.",
	"Eel spawning closures on feeder rivers restricted barge transits at night.",
	"Rogue privateers levied shadow tolls on coastal routes, deterring shippers.",
	"A warehouse fire in the incense quarter contaminated adjacent cargoes.",
	"A court injunction over contested wreck salvage tied up berths for days.",
	"Unexpected monsoon surges sank two coastal lighters, scaring lenders and reinsurers.",
}

local function popUniqueReason(list)
	if #list == 0 then
		return "(No reason: out of reasons!)"
	end
	local i = math.random(#list)
	local r = list[i]
	table.remove(list, i)
	return r
end

---------------------------------------------------------
-- Safe require (cached)
---------------------------------------------------------
local _req = {}
local function safeRequire(name)
	if _req[name] ~= nil then return _req[name] end
	local ok, mod = pcall(require, name)
	if not ok then mod = false end
	_req[name] = mod
	return mod
end

---------------------------------------------------------
-- Signal extraction (once per render)
---------------------------------------------------------
local function parseOutbreakSeverity(outText)
	outText = tostring(outText or "")
	local sev = outText:match('style="background:[^"]+"%s*|%s*([A-Z]+)%s*||')
	if sev == "CRITICAL" or sev == "HIGH" or sev == "MEDIUM" or sev == "LOW" or sev == "NONE" then
		return sev
	end
	return "NONE"
end

local function severityToRisk(sev)
	if sev == "LOW" then return 0.20 end
	if sev == "MEDIUM" then return 0.45 end
	if sev == "HIGH" then return 0.70 end
	if sev == "CRITICAL" then return 0.95 end
	return 0.0
end

local function opsLevelToRisk(level)
	level = tonumber(level) or 0
	if level <= 0 then return 0.0 end
	if level == 1 then return 0.25 end
	if level == 2 then return 0.55 end
	return 0.85
end

local function clamp(x, lo, hi)
	if x < lo then return lo end
	if x > hi then return hi end
	return x
end

local function computeSignals(hubCity, psscYear, dayOfYear)
	local sig = {
		hub = hubCity,
		wxRisk = 0.0,
		wxLabel = "Green",
		wxCond = "",
		warRisk = 0.0,
		hatchIntensity = 0.0,
		templeRedRate = 0.0,
		healthRisk = 0.0,
		mood = 0.0,
		vol = 0.0,
	}

	-- Weather / Ops
	local forecast = safeRequire("Module:BassaridiaForecast")
	if forecast and type(forecast.getOpsData) == "function" then
		local op = forecast.getOpsData(hubCity, psscYear, dayOfYear)
		if op and op.alert then
			sig.wxLabel = tostring(op.alert.label or "Green")
			sig.wxCond  = tostring(op.condition or "")
			sig.wxRisk  = opsLevelToRisk(op.alert.level)
			local imp = op.impacts or {}
			if tostring(imp.ground or "") == "Closed" then sig.wxRisk = sig.wxRisk + 0.10 end
			if tostring(imp.sea or "") == "Closed" then sig.wxRisk = sig.wxRisk + 0.10 end
			if tostring(imp.air or "") == "No-Go" then sig.wxRisk = sig.wxRisk + 0.05 end
			sig.wxRisk = clamp(sig.wxRisk, 0.0, 1.0)
		end
	end

	-- War League tempo
	local wl = safeRequire("Module:WarLeagueDivisionOpsBVR")
	if wl and type(wl.makeTable) == "function" then
		local out = tostring(wl.makeTable({ args = { rows = "48", showlegend = "no", seed = tostring(psscYear * 1000 + dayOfYear) } }) or "")
		local rows, high, critical = 0, 0, 0
		for sev in out:gmatch("||%s*(LOW|MEDIUM|HIGH|CRITICAL)%s*||") do
			rows = rows + 1
			if sev == "HIGH" then high = high + 1 end
			if sev == "CRITICAL" then critical = critical + 1 end
		end
		if rows > 0 then
			local score = (high + 2 * critical) / rows
			sig.warRisk = clamp(score / 2, 0.0, 1.0)
		end
	end

	-- Hatch Ministry tempo
	local hatch = safeRequire("Module:HatchMinistryDailyOps")
	if hatch and type(hatch.dailyReport) == "function" then
		local rep = tostring(hatch.dailyReport({ args = { year = tostring(psscYear), day = tostring(dayOfYear), legend = "no" } }) or "")
		local k = 0
		for _ in rep:gmatch("[Ii]nterdict") do k = k + 1 end
		for _ in rep:gmatch("[Ii]nspect") do k = k + 1 end
		for _ in rep:gmatch("[Ss]eiz") do k = k + 1 end
		for _ in rep:gmatch("blockade") do k = k + 1 end
		sig.hatchIntensity = clamp(k / 40, 0.0, 1.0)
	end

	-- Temple missionary tempo
	local miss = safeRequire("Module:MissionaryDailyOpsBVR")
	if miss and type(miss.makeTable) == "function" then
		local out = tostring(miss.makeTable({ args = { region = "Core", includeThreat = "no", showEcon = "no", limit = "40", showlegend = "no" } }) or "")
		local red, n = 0, 0
		for sev in out:gmatch("||%s*(Green|Yellow|Red)%s*$") do
			n = n + 1
			if sev == "Red" then red = red + 1 end
		end
		if n > 0 then sig.templeRedRate = clamp(red / n, 0.0, 1.0) end
	end

	-- Public health
	local outb = safeRequire("Module:BassaridiaOutbreaks")
	if outb and type(outb.outbreaksForCity) == "function" then
		local txt = outb.outbreaksForCity({ args = { city = hubCity } })
		local sev = parseOutbreakSeverity(txt)
		sig.healthRisk = severityToRisk(sev)
	end

	-- Mood [-1,1]
	local mood = 0.0
	mood = mood - (sig.wxRisk * 0.35)
	mood = mood - (sig.warRisk * 0.30)
	mood = mood - (sig.healthRisk * 0.30)
	mood = mood + (sig.templeRedRate * 0.12)
	mood = mood - (sig.hatchIntensity * 0.05)
	sig.mood = clamp(mood, -1.0, 1.0)

	-- Volatility [0,1]
	local vol = 0.0
	vol = vol + sig.wxRisk * 0.40
	vol = vol + sig.warRisk * 0.35
	vol = vol + sig.hatchIntensity * 0.20
	vol = vol + sig.healthRisk * 0.35
	sig.vol = clamp(vol, 0.0, 1.0)

	return sig
end

---------------------------------------------------------
-- Company-specific sensitivity (small, bounded)
---------------------------------------------------------
local function companyBias(co, sig)
	local sym = tostring(co.symbol or "")
	local name = tostring(co.name or ""):lower()

	if sym == "HMIN" or name:find("hatch ministry", 1, true) then
		return clamp((sig.hatchIntensity * 0.10) + (sig.warRisk * 0.06) - (sig.wxRisk * 0.04) - (sig.healthRisk * 0.04), -0.20, 0.20)
	end

	if sym == "SMOR" or name:find("moorland", 1, true) then
		return clamp(-(sig.warRisk * 0.08) - (sig.hatchIntensity * 0.06) - (sig.healthRisk * 0.05), -0.20, 0.20)
	end

	if name:find("port of", 1, true) or sym == "STRC" then
		return clamp(-(sig.wxRisk * 0.08) - (sig.healthRisk * 0.04) + (sig.templeRedRate * 0.03), -0.20, 0.20)
	end

	return clamp((sig.templeRedRate * 0.02) - (sig.wxRisk * 0.03) - (sig.warRisk * 0.02) - (sig.healthRisk * 0.02), -0.12, 0.12)
end

local function companyVolScale(co)
	local name = tostring(co.name or ""):lower()
	if name:find("port of", 1, true) then return 1.15 end
	if name:find("fund", 1, true) then return 0.90 end
	return 1.0
end

local function signalPrefix(direction, co, sig)
	local parts = {}
	if sig.wxRisk >= 0.60 then parts[#parts+1] = "Weather/ops constraints" end
	if sig.warRisk >= 0.55 then parts[#parts+1] = "War League posture" end
	if sig.healthRisk >= 0.55 then parts[#parts+1] = "Public health pressure" end
	if sig.hatchIntensity >= 0.55 then parts[#parts+1] = "Hatch enforcement tempo" end
	if sig.templeRedRate >= 0.35 then parts[#parts+1] = "Temple mission tempo" end

	if (co.symbol == "HMIN" or tostring(co.name or ""):lower():find("hatch ministry", 1, true)) and sig.hatchIntensity >= 0.40 then
		parts[#parts+1] = "Enforcement rents"
	end

	if #parts == 0 then
		return "Signals: routine conditions."
	end
	return "Signals: " .. table.concat(parts, "; ") .. "."
end

---------------------------------------------------------
-- Main function: produce the table (signal-driven)
---------------------------------------------------------
function p.generateTable(frame)
	local args = (frame and frame.args) or {}
	local hub = tostring(args.hub or "Vaeringheim")
	local sharePrice = tonumber(args.shareprice or 30) or 30

	local dateString, dayOfYear, psscYear = getDailyDate()
	local localTime = os.date("*t")
	local dailySeed = localTime.year * 1000 + localTime.yday
	math.randomseed(dailySeed)

	-- Copy reasons
	local incPool, decPool = {}, {}
	for _, r in ipairs(increaseReasons) do incPool[#incPool+1] = r end
	for _, r in ipairs(decreaseReasons) do decPool[#decPool+1] = r end

	-- Signals once
	local sig = computeSignals(hub, psscYear, dayOfYear)

	local out = {}
	table.insert(out, '{| class="wikitable" style="text-align:center; width:100%;"')
	table.insert(out, '|-')
	table.insert(out, '! Date !! Symbol !! Company !! Open (Polis) !! Close (Polis) !! Volume !! Change !! Change % !! Reason')

	for _, co in ipairs(companies) do
		local stocksStart = tonumber(co.stocks) or 1
		if stocksStart < 1 then stocksStart = 1 end

		local bias = companyBias(co, sig)
		local pInc = clamp(0.50 + (sig.mood * 0.18) + bias, 0.12, 0.88)
		local direction = (math.random() < pInc) and "Inc" or "Dec"

		local volScale = companyVolScale(co)
		local volFactor = clamp(0.55 + sig.vol * 0.90 * volScale, 0.45, 1.30)
		local maxDelta = math.floor(stocksStart * 0.30 * volFactor)
		if maxDelta < 1 then maxDelta = 1 end

		local buy, sell = 0, 0
		if direction == "Inc" then
			buy  = math.random(1, maxDelta)
			sell = math.random(0, buy - 1)
		else
			sell = math.random(1, maxDelta)
			buy  = math.random(0, sell - 1)
		end

		local stocksEnd = stocksStart + buy - sell

		local openVal  = stocksStart * sharePrice
		local closeVal = stocksEnd   * sharePrice
		local volume = buy + sell
		local changeVal = closeVal - openVal

		local pctChange = 0
		if openVal > 0 then
			pctChange = (changeVal / openVal) * 100
		end

		local baseReason
		if changeVal > 0 then
			baseReason = popUniqueReason(incPool)
		elseif changeVal < 0 then
			baseReason = popUniqueReason(decPool)
		else
			baseReason = popUniqueReason(incPool)
		end

		local reason = signalPrefix(direction, co, sig) .. " " .. baseReason

		table.insert(out, table.concat({
			'|-',
			string.format('| %s || %s || %s || %d || %d || %d || %+d || %.2f%% || %s',
				dateString,
				co.symbol,
				co.name,
				openVal,
				closeVal,
				volume,
				changeVal,
				pctChange,
				reason
			)
		}, '\n'))
	end

	table.insert(out, '|}')
	return table.concat(out, '\n')
end

return p