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: BassaridiaDailyInvestorTable
-- Module: BassaridiaDailyInvestorTable
-- Updates each day with new buy/sell, but uses the
-- Generates a daily table of investor stock activity
-- "previous day" simulation to set 'Stocks at Start'
-- and associated reasons for increases/decreases.
-- to 'Stocks at End' from that previous day.
--
--
-- NOTES:
-- Each new real-life day:
-- 1) Because we do not store data externally, we
-- 1) We use the current date to set a random seed,
--   re-simulate "yesterday" each day with a slightly
--     so the simulation is stable for that day,
--   different random seed. Then we use that to set the
--     but refreshes on the next day.
--   starting stocks for "today."
-- 2) About half the investors have net share "Inc"
-- 2) Everything is still stable for the current day, but
--     (buy > sell), half "Dec" (sell > buy).
--   tomorrow it re-simulates again. Therefore, actual
-- 3) No investor's net change in shares (stocksAtEnd
--   day-to-day changes remain consistent in the script's
--     - stocksAtStart) may exceed 30% of stocksAtStart.
--   logic, *so long as* no additional changes are made.
-- 4) Each investor draws a unique reason from the
--     appropriate pool (increase vs. decrease).
---------------------------------------------------------
---------------------------------------------------------
local p = {}
local p = {}


---------------------------------------------------------
---------------------------------------------------------
-- 1. Calendar: Determine PSSC + daily seeds
-- 1. getCurrentDate
--    Returns "DD/MM/YYYY PSSC" plus dayOfYear
---------------------------------------------------------
---------------------------------------------------------
local function getCurrentDate()
local function getCurrentDate()
     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 currentDate = os.time()
     local now = os.time()
     local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay)
     local totalDaysElapsed = math.floor((now - startDate) / secondsInDay)


     local yearFraction = totalDaysElapsed / daysPerYear
     local yearFraction = totalDaysElapsed / daysPerYear
     local psscYear     = math.floor(yearFraction)
     local psscYear = math.floor(yearFraction)
     local dayOfYear   = math.floor((yearFraction - psscYear) * daysPerYear) + 1
     local dayOfYear = math.floor((yearFraction - psscYear) * daysPerYear) + 1


     -- For demonstration, we do 3 months: 1..61, 62..122, 123..183
     -- simplistic 3-month model
     local month, dayOfMonth
     local month, dayOfMonth
     if dayOfYear <= 61 then
     if dayOfYear <= 61 then
Line 45: Line 47:
     end
     end


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


---------------------------------------------------------
---------------------------------------------------------
-- 2. Investor Data (Initial "Base" Stocks)
-- 2. Investor Data
---------------------------------------------------------
--   (Each investor has a 'stocks' starting point.)
-- This table holds each investor's *base* stock count,
-- used only if we had no prior day. We'll override each
-- day using the "simulateYesterday" logic below.
---------------------------------------------------------
---------------------------------------------------------
local baseInvestors = {
local investors = {
     {name = "Vaeringheim Regional Investor", stocks = 17424},
     { name = "Vaeringheim Regional Investor",     stocks = 17424 },
     {name = "Alpazkigz Regional Investor",   stocks = 16214},
     { name = "Alpazkigz Regional Investor",       stocks = 16214 },
     {name = "Odiferia Regional Investor",   stocks = 14718},
     { name = "Odiferia Regional Investor",       stocks = 14718 },
     {name = "Hafaan Regional Investor",     stocks =  3630},
     { name = "Hafaan Regional Investor",         stocks =  3630 },
     {name = "Jeseri Regional Investor",     stocks =  1870},
     { name = "Jeseri Regional Investor",         stocks =  1870 },
     {name = "Hatch Ministry Regional Investor", stocks = 7810},
     { name = "Hatch Ministry Regional Investor", stocks = 7810 },
     {name = "New South Jangsong Division",   stocks = 11085},
     { name = "New South Jangsong Division",       stocks = 11085 },
     {name = "Haifan Bassaridia Division",   stocks = 12500},
     { name = "Haifan Bassaridia Division",       stocks = 12500 },
}
}


---------------------------------------------------------
---------------------------------------------------------
-- 3. Reasons for Increase or Decrease
-- 3. Reasons for Increase / Decrease
---------------------------------------------------------
-- Enough reasons so that each investor can have a unique one.
---------------------------------------------------------
---------------------------------------------------------
local increaseReasons = {
local increaseReasons = {
     "Improved logistics attracted new investors.",
     "Improved logistics at the General Port attracted new investors.",
     "An influx of goods boosted trade volume.",
     "An influx of goods from Delphica boosted trade volume at the port.",
     "High demand for Noctic-Rabrev exports raised confidence.",
     "High demand for Noctic-Rabrev exports raised investor confidence.",
     "Completion of a new shipping lane 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.",
     "A local festival drove significant port activity.",
     "Festival of Light in Vaeringheim drove significant port activity.",
     "High attendance at a major trade symposium enhanced the port's reputation.",
     "High attendance at the Bassaridia Trade Symposium enhanced the port's reputation.",
     "Discovery of new mineral resources increased export potential.",
     "Discovery of new mineral resources near Acheron increased export potential.",
     "A surge in ceremonial wine exports bolstered port operations.",
     "A surge in ceremonial wine exports from the Blood Vineyards bolstered port ops.",
     "Efficient cargo handling innovations improved investor sentiment.",
     "Efficient cargo handling improvements boosted investor sentiment.",
     "Seasonal agricultural exports saw a sharp increase.",
     "Seasonal agricultural exports from Plains of Jogi saw a sharp increase.",
     "High sales of pearl and abalone products boosted port activity.",
     "High sales of pearl and abalone products boosted port traffic.",
     "Introduction of new luxury goods attracted high-end investors.",
     "Luxury goods from Tel-Amin attracted new high-end investors.",
     "Expansion of the port's capacity improved its competitiveness.",
     "Port capacity expansion in Suncliff improved competitiveness.",
     "Improved relations with local merchant guilds stabilized trade routes.",
     "Improved relations with Maskmakers Guild stabilized trade routes.",
     "Increased fishing yields led to greater port activity.",
     "Increased fishing yields around Lake Morovia led to more port activity.",
     "Enhanced security measures reduced piracy concerns.",
     "Enhanced security measures at the Port reduced piracy concerns.",
     "Growing cultural appreciation for exotic exports boosted confidence.",
     "Growing cultural appreciation for sacred Horehound exports boosted confidence.",
     "Trade agreements with neighboring cities expanded port usage.",
     "Trade deals with Saluria expanded port usage significantly.",
     "Launch of new fertilizer shipments attracted agricultural investors.",
    "Demand for silk from the Seamstresses of Rouge improved port revenue.",
     "Rising global interest in sacred resin exports spiked port activity.",
    "Moonlit Tide celebrations increased ceremonial goods traffic.",
     "Successful marketing campaigns highlighted the port's strategic importance.",
     "Launch of new Garganid fertilizer shipments attracted agriculture investors.",
     "Completion of high-speed rail connections improved trade efficiency.",
     "Global interest in sacred resin exports enhanced port shipments.",
     "Successful marketing campaign highlighted the Port's location.",
     "Completion of high-speed rail connections boosted trade efficiency."
}
}


local decreaseReasons = {
local decreaseReasons = {
     "Pirate raids in the Strait disrupted shipments.",
     "Pirate raids in the Strait of Haifa disrupted shipments to the Port.",
     "Unfavorable weather delayed agricultural exports.",
     "Unfavorable weather delayed agriculture exports from Plains of Jogi.",
     "Labor shortages caused delays in cargo handling.",
     "Labor shortages in the Port caused cargo-handling delays.",
     "A major fire in supply regions reduced exports.",
     "A major fire in Salurian sacred groves reduced resin exports.",
     "Rising costs impacted investor confidence.",
     "Rising costs of Noctic-Rabrev cultivation hurt confidence.",
     "Unrest in Jangsong Province disrupted shipments.",
     "Unrest in Jangsong Province disrupted camel meat shipments.",
     "Currency fluctuations affected trade 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 reduced goods traffic.",
     "Decline in ceremonial artifact production lowered traffic.",
     "Cultural disputes caused delays in exports.",
     "Cultural disputes over Horehound caused export delays.",
     "Counterfeit relics discovered harmed the port's reputation.",
     "Counterfeit relics in Nexa harmed the port's reputation.",
     "High demand outpaced supply, reducing exports.",
     "Fishing yields were outpaced by demand, reducing shipments.",
     "Decline in pearl quality lowered investor interest.",
     "Decline in pearl quality from Anterran sources lowered investor interest.",
     "Shortage of ceremonial wine hurt exports.",
     "Shortage of ceremonial wine from Blood Vineyards cut exports.",
     "Unrest among key merchants disrupted major trade agreements.",
     "Unrest among Maskmaker merchants disrupted trade agreements.",
     "Reduced demand for local textiles impacted port activity.",
     "Less demand for silk from the Seamstresses of Rouge impacted port flows.",
     "Collapse of a key trade deal reduced port volume.",
     "Collapse of trade agreement with Tel-Amin lowered volume.",
     "Seasonal labor shortages decreased shipments.",
     "Seasonal labor shortages shrank port shipments significantly.",
     "Alternate shipping routes bypassed the port.",
     "Disruptions in Suncliff Fisheries slowed aquatic exports.",
     "Economic downturn in a key region affected investor interest.",
    "Alternative shipping routes bypassed the Port altogether.",
     "Piracy concerns in Haifan waters harmed trade confidence.",
     "Economic downturn in Lunalis Sancta reduced investor interest.",
     "Failure to secure deals at a trade symposium reduced traffic.",
     "Lingering piracy concerns in Haifan waters harmed confidence.",
     "Natural disasters destroyed key trade routes.",
     "Failure at Bassaridia Trade Symposium lowered traffic.",
     "Sabotage of port infrastructure caused significant delays.",
     "Natural disasters near Acheron destroyed key routes.",
     "Sabotage of port infrastructure caused shipping delays."
}
}


---------------------------------------------------------
---------------------------------------------------------
-- 4. Tiers
-- 4. Utility: pick a random reason, ensuring no duplicates
---------------------------------------------------------
---------------------------------------------------------
local function determineTier(stocksAtEnd)
local function popUniqueReason(list)
     if stocksAtEnd > 12500 then
     if #list == 0 then
        return "Tier I"
         return "No reason available (out of reasons)."
    elseif stocksAtEnd >= 5000 then
         return "Tier II"
    else
        return "Tier III"
     end
     end
    local rnd = math.random(#list)
    local chosen = list[rnd]
    table.remove(list, rnd)
    return chosen
end
end


---------------------------------------------------------
---------------------------------------------------------
-- 5. Single-Day Simulation
-- 5. Force half to "inc" half to "dec"
---------------------------------------------------------
---------------------------------------------------------
local function simulateDailyActivity(stocksAtStart)
local function assignUpDown(count)
     local buyStocks = math.random(0, 500)
     -- If count=8, we want 4 inc, 4 dec
     local sellStocks = math.random(0, 500)
     local half = math.floor(count / 2)
 
     local arr = {}
     local stocksAtEnd = stocksAtStart + buyStocks - sellStocks
     for i=1, half do
     local change = "Increased"
         arr[i] = "Inc"
    if stocksAtEnd < stocksAtStart then
     end
         change = "Decreased"
    for i=half+1, count do
     elseif stocksAtEnd == stocksAtStart then
         arr[i] = "Dec"
         change = "No Change"
     end
     end
 
     -- shuffle
     return {
     for i=#arr,2,-1 do
        buyStocks     = buyStocks,
       local j=math.random(i)
        sellStocks    = sellStocks,
      arr[i], arr[j] = arr[j], arr[i]
        stocksAtEnd  = stocksAtEnd,
        change       = change,
    }
end
 
local function getReason(change)
    if change == "Increased" then
        return increaseReasons[math.random(#increaseReasons)]
    elseif change == "Decreased" then
        return decreaseReasons[math.random(#increaseReasons)]
    else
        return "Market activity remained stable."
     end
     end
    return arr
end
end


---------------------------------------------------------
---------------------------------------------------------
-- 6. Yesterday Simulation:
-- 6. generateTable function
--    1) We define a "yesterSeed" = dailySeed - 1
--    2) We replicate the random buy/sell from "yesterday"
--    3) We get stocksAtEndFromYesterday
--    4) That becomes today's StocksAtStart
---------------------------------------------------------
---------------------------------------------------------
local function getYesterdaysEnd(investorBase, dailySeed)
function p.generateTable(frame)
    local yesterSeed = dailySeed - 1
    math.randomseed(yesterSeed)
 
    -- We'll simulate the same logic we use for the day,
    -- but only for "yesterday"
    local baseStart = investorBase.stocks
    local yResult  = simulateDailyActivity(baseStart)
    return yResult.stocksAtEnd  -- "yesterday's" stocks at end
end


---------------------------------------------------------
     local dateString, dayOfYear = getCurrentDate()
-- 7. Main Table Generation
    -- daily random seed
---------------------------------------------------------
function p.generateTable()
    -- 7.1) Today’s date + seeds
     local dateString, psscYear, dayOfYear = getCurrentDate()
     local localTime = os.date("*t")
     local localTime = os.date("*t")
     local dailySeed = localTime.year * 1000 + localTime.yday
     local dailySeed = localTime.year * 1000 + localTime.yday
Line 198: Line 173:


     local stockPrice = 30
     local stockPrice = 30
    local totalMarketValueAtStart = 0
    local totalMarketValueAtEnd  = 0
    local totalBoughtValue        = 0
    local totalSoldValue          = 0


     local out = {}
    -- HEADERS
    table.insert(out, '{| class="wikitable"\n')
     local out = '{| class="wikitable"\n'
     table.insert(out,
     out = out .. "! Date "
        "! Date (Day/Month/Year PSSC) !! Investor !! Tier !! Stocks at Start !! Value at Start !! Stocks Bought !! Value of Stocks Bought !! Stocks Sold !! Value of Stocks Sold !! Stocks at End !! Value at End !! Change !! Reason\n"
    out = out .. "! Investor "
     )
    out = out .. "! Stocks at Start "
    out = out .. "! Stocks Bought "
    out = out .. "! Stocks Sold "
    out = out .. "! Stocks at End "
    out = out .. "! Value at Start "
    out = out .. "! Value at End "
    out = out .. "! Change "
    out = out .. "! Reason\n"
 
    local n = #investors
     local updown = assignUpDown(n)
 
    local totalStartValue = 0
    local totalEndValue  = 0
 
    -- Copy the reason arrays so we don't inadvertently remove them from the original
    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
 
    for i, inv in ipairs(investors) do
        local stocksStart = inv.stocks
        local label = updown[i] -- "Inc" or "Dec"
 
        -- We want to ensure final change won't exceed 30%.
        local maxDelta = math.floor(stocksStart * 0.3)  -- e.g. 30% of start
        if maxDelta < 1 then maxDelta=1 end
 
        local buy=0
        local sell=0


    -- For each investor
        if label == "Inc" then
    for _, baseObj in ipairs(baseInvestors) do
            -- net up => buy - sell > 0
        local yEnd = getYesterdaysEnd(baseObj, dailySeed)
            -- pick buy up to 30% (1..maxDelta), sell up to (buy-1)
         local stocksAtStart = yEnd  -- "today's start" is "yesterday's end"
            buy = math.random(1, maxDelta)
        if stocksAtStart < 0 then
            sell = math.random(0, buy-1)
             stocksAtStart = 0 -- safety clamp
         else
            -- net down => sell - buy > 0
            -- pick sell up to 30% (1..maxDelta), buy up to (sell-1)
            sell = math.random(1, maxDelta)
             buy = math.random(0, sell-1)
         end
         end


         local valueAtStart = stocksAtStart * stockPrice
         local stocksEnd = stocksStart + buy - sell
        local valueStart = stocksStart * stockPrice
        local valueEnd  = stocksEnd  * stockPrice
        totalStartValue = totalStartValue + valueStart
        totalEndValue  = totalEndValue  + valueEnd


         -- Re-randomseed for "today"
         local changeWord
         math.randomseed(dailySeed)
        if stocksEnd > stocksStart then
            changeWord = "Increased"
        elseif stocksEnd < stocksStart then
            changeWord = "Decreased"
        else
            changeWord = "No Change"
         end


         local dayResult = simulateDailyActivity(stocksAtStart)
        -- pick reason from incPool or decPool or incPool if no change
         local buyCount  = dayResult.buyStocks
         local reason
        local sellCount = dayResult.sellStocks
        if changeWord == "Increased" then
        local stocksAtEnd = dayResult.stocksAtEnd
            reason = popUniqueReason(incPool)
         if stocksAtEnd < 0 then
         elseif changeWord == "Decreased" then
             stocksAtEnd = 0
            reason = popUniqueReason(decPool)
         else
             reason = popUniqueReason(incPool) -- or decPool, up to you
         end
         end
        local valueBought = buyCount  * stockPrice
        local valueSold  = sellCount * stockPrice
        local valueAtEnd  = stocksAtEnd * stockPrice
        local theChange  = dayResult.change
        local reason      = getReason(theChange)
        local tier        = determineTier(stocksAtEnd)
        -- accumulate totals
        totalMarketValueAtStart = totalMarketValueAtStart + valueAtStart
        totalMarketValueAtEnd  = totalMarketValueAtEnd  + valueAtEnd
        totalBoughtValue        = totalBoughtValue        + valueBought
        totalSoldValue          = totalSoldValue          + valueSold


         table.insert(out, "|-\n")
         out = out .. string.format(
        table.insert(out, string.format(
[[
            "| %s || %s || %s || %d || %d Polis || %d || %d Polis || %d || %d Polis || %d || %d Polis || %s || %s\n",
|-
             dateString,         -- today's date
| %s
             baseObj.name,
| %s
             tier,
| %d
            stocksAtStart,
| %d
            valueAtStart,
| %d
            buyCount,
| %d
             valueBought,
| %d Polis
             sellCount,
| %d Polis
             valueSold,
| %s
             stocksAtEnd,
| %s
             valueAtEnd,
]],
             theChange,
             dateString,
             inv.name,
             stocksStart,
             buy,
             sell,
             stocksEnd,
             valueStart,
             valueEnd,
             changeWord,
             reason
             reason
         ))
         )
     end
     end


     -- Totals row
     -- Totals row
     table.insert(out, "|-\n")
     out = out .. string.format(
    table.insert(out, string.format(
[[
        "| colspan=5 | Total Market Value at Start || %d Polis || %d Polis || %d Polis || %d Polis || ||\n",
|-
         totalMarketValueAtStart, totalBoughtValue, totalSoldValue, totalMarketValueAtEnd
| colspan="2" | Totals
     ))
|  
     table.insert(out, "|}")
|  
|  
|  
| %d Polis
| %d Polis
|  
|  
]],
         totalStartValue,
        totalEndValue
     )
 
     out = out .. "\n|}"


     return table.concat(out)
     return out
end
end


return p
return p

Revision as of 17:25, 27 December 2024

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

---------------------------------------------------------
-- Module: BassaridiaDailyInvestorTable
-- Generates a daily table of investor stock activity
-- and associated reasons for increases/decreases.
--
-- Each new real-life day:
--  1) We use the current date to set a random seed,
--     so the simulation is stable for that day,
--     but refreshes on the next day.
--  2) About half the investors have net share "Inc"
--     (buy > sell), half "Dec" (sell > buy).
--  3) No investor's net change in shares (stocksAtEnd
--     - stocksAtStart) may exceed 30% of stocksAtStart.
--  4) Each investor draws a unique reason from the
--     appropriate pool (increase vs. decrease).
---------------------------------------------------------

local p = {}

---------------------------------------------------------
-- 1. getCurrentDate
--    Returns "DD/MM/YYYY PSSC" plus dayOfYear
---------------------------------------------------------
local function getCurrentDate()
    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

    -- simplistic 3-month model
    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
end

---------------------------------------------------------
-- 2. Investor Data
--    (Each investor has a 'stocks' starting point.)
---------------------------------------------------------
local investors = {
    { name = "Vaeringheim Regional Investor",     stocks = 17424 },
    { name = "Alpazkigz Regional Investor",       stocks = 16214 },
    { name = "Odiferia Regional Investor",        stocks = 14718 },
    { name = "Hafaan Regional Investor",          stocks =  3630 },
    { name = "Jeseri Regional Investor",          stocks =  1870 },
    { name = "Hatch Ministry Regional Investor",  stocks =  7810 },
    { name = "New South Jangsong Division",       stocks = 11085 },
    { name = "Haifan Bassaridia Division",        stocks = 12500 },
}

---------------------------------------------------------
-- 3. Reasons for Increase / Decrease
---------------------------------------------------------
-- Enough reasons so that each investor can have a unique one.
---------------------------------------------------------
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 the port's reputation.",
    "Discovery of new mineral resources near Acheron increased export potential.",
    "A surge in ceremonial wine exports from the Blood Vineyards bolstered port ops.",
    "Efficient cargo handling improvements boosted investor sentiment.",
    "Seasonal agricultural exports from Plains of 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.",
    "Growing cultural appreciation for sacred 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 port shipments.",
    "Successful marketing campaign highlighted the Port's location.",
    "Completion of high-speed rail connections boosted trade efficiency."
}

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 investor interest.",
    "Shortage of ceremonial wine from Blood Vineyards cut exports.",
    "Unrest among Maskmaker merchants disrupted trade agreements.",
    "Less demand for silk from the Seamstresses of Rouge impacted port 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."
}

---------------------------------------------------------
-- 4. Utility: pick a random reason, ensuring no duplicates
---------------------------------------------------------
local function popUniqueReason(list)
    if #list == 0 then
        return "No reason available (out of reasons)."
    end
    local rnd = math.random(#list)
    local chosen = list[rnd]
    table.remove(list, rnd)
    return chosen
end

---------------------------------------------------------
-- 5. Force half to "inc" half to "dec"
---------------------------------------------------------
local function assignUpDown(count)
    -- If count=8, we want 4 inc, 4 dec
    local half = math.floor(count / 2)
    local arr = {}
    for i=1, half do
        arr[i] = "Inc"
    end
    for i=half+1, count do
        arr[i] = "Dec"
    end
    -- shuffle
    for i=#arr,2,-1 do
       local j=math.random(i)
       arr[i], arr[j] = arr[j], arr[i]
    end
    return arr
end

---------------------------------------------------------
-- 6. generateTable function
---------------------------------------------------------
function p.generateTable(frame)

    local dateString, dayOfYear = getCurrentDate()
    -- daily random seed
    local localTime = os.date("*t")
    local dailySeed = localTime.year * 1000 + localTime.yday
    math.randomseed(dailySeed)

    local stockPrice = 30

    -- HEADERS
    local out = '{| class="wikitable"\n'
    out = out .. "! Date "
    out = out .. "! Investor "
    out = out .. "! Stocks at Start "
    out = out .. "! Stocks Bought "
    out = out .. "! Stocks Sold "
    out = out .. "! Stocks at End "
    out = out .. "! Value at Start "
    out = out .. "! Value at End "
    out = out .. "! Change "
    out = out .. "! Reason\n"

    local n = #investors
    local updown = assignUpDown(n)

    local totalStartValue = 0
    local totalEndValue   = 0

    -- Copy the reason arrays so we don't inadvertently remove them from the original
    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

    for i, inv in ipairs(investors) do
        local stocksStart = inv.stocks
        local label = updown[i] -- "Inc" or "Dec"

        -- We want to ensure final change won't exceed 30%.
        local maxDelta = math.floor(stocksStart * 0.3)  -- e.g. 30% of start
        if maxDelta < 1 then maxDelta=1 end

        local buy=0
        local sell=0

        if label == "Inc" then
            -- net up => buy - sell > 0
            -- pick buy up to 30% (1..maxDelta), sell up to (buy-1)
            buy = math.random(1, maxDelta)
            sell = math.random(0, buy-1)
        else
            -- net down => sell - buy > 0
            -- pick sell up to 30% (1..maxDelta), buy up to (sell-1)
            sell = math.random(1, maxDelta)
            buy = math.random(0, sell-1)
        end

        local stocksEnd = stocksStart + buy - sell
        local valueStart = stocksStart * stockPrice
        local valueEnd   = stocksEnd   * stockPrice
        totalStartValue = totalStartValue + valueStart
        totalEndValue   = totalEndValue   + valueEnd

        local changeWord
        if stocksEnd > stocksStart then
            changeWord = "Increased"
        elseif stocksEnd < stocksStart then
            changeWord = "Decreased"
        else
            changeWord = "No Change"
        end

        -- pick reason from incPool or decPool or incPool if no change
        local reason
        if changeWord == "Increased" then
            reason = popUniqueReason(incPool)
        elseif changeWord == "Decreased" then
            reason = popUniqueReason(decPool)
        else
            reason = popUniqueReason(incPool) -- or decPool, up to you
        end

        out = out .. string.format(
[[
|-
| %s
| %s
| %d
| %d
| %d
| %d
| %d Polis
| %d Polis
| %s
| %s
]],
            dateString,
            inv.name,
            stocksStart,
            buy,
            sell,
            stocksEnd,
            valueStart,
            valueEnd,
            changeWord,
            reason
        )
    end

    -- Totals row
    out = out .. string.format(
[[
|-
| colspan="2" | Totals
| 
| 
| 
| 
| %d Polis
| %d Polis
| 
| 
]],
        totalStartValue,
        totalEndValue
    )

    out = out .. "\n|}"

    return out
end

return p