/* MI newsletter automation v1.0 beta. Pure parsing and validation; no network. */ var MINewsletter = (function () { "use strict"; function requireThat(ok, reason) { if (!ok) throw new Error(reason); } function text(html) { return html.replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, '') .replace(/<[^>]*>/g, ' ').replace(/"/g, '"').replace(/'|'/g, "'") .replace(/</g, '<').replace(/>/g, '>').replace(/ /g, ' ') .replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(parseInt(n, 16))) .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n))).replace(/&/g, '&'); } function canonical(v) { if (Array.isArray(v)) return '[' + v.map(canonical).join(',') + ']'; if (v && typeof v === 'object') return '{' + Object.keys(v).sort().map(k => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}'; return JSON.stringify(v); } // Parse JSON without silently accepting duplicate keys. Never eval mail content. function parseAt(s, start) { let i = start; function ws() { while (/\s/.test(s[i] || '') && i < s.length) i++; } function val(depth) { requireThat(depth < 35, 'JSON nesting limit'); ws(); if (s[i] === '"') { const a = i++; while (i < s.length) { if (s[i] === '\\') { i += 2; continue; } if (s[i++] === '"') return JSON.parse(s.slice(a, i)); } throw new Error('Unterminated JSON string'); } if (s[i] === '{') { i++; ws(); const o = Object.create(null); if (s[i] === '}') { i++; return o; } for (;;) { requireThat(s[i] === '"', 'Invalid JSON key'); const k = val(depth + 1); ws(); requireThat(!Object.prototype.hasOwnProperty.call(o, k), 'Duplicate JSON key'); requireThat(s[i++] === ':', 'Invalid JSON separator'); o[k] = val(depth + 1); ws(); if (s[i++] === '}') return o; requireThat(s[i - 1] === ',', 'Invalid JSON object'); ws(); } } if (s[i] === '[') { i++; ws(); const a = []; if (s[i] === ']') { i++; return a; } for (;;) { a.push(val(depth + 1)); ws(); if (s[i++] === ']') return a; requireThat(s[i - 1] === ',', 'Invalid JSON array'); } } const m = /^(true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(s.slice(i)); requireThat(m, 'Invalid JSON value'); i += m[0].length; const v = JSON.parse(m[0]); requireThat(typeof v !== 'number' || Number.isFinite(v), 'Nonfinite JSON'); return v; } return {value: val(0), end: i}; } function keys(o, expected) { requireThat(o && !Array.isArray(o) && typeof o === 'object' && Object.keys(o).sort().join('|') === expected.slice().sort().join('|'), 'Unsupported or missing JSON fields'); } function day(s) { requireThat(typeof s === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(s) && new Date(s + 'T12:00:00Z').toISOString().slice(0,10) === s, 'Invalid date'); return s; } function number(v, name) { const n=Number(v); requireThat(v !== '' && v !== null && typeof v !== 'boolean' && Number.isFinite(n), 'Invalid ' + name); return n; } const rules = {order:'exits_before_entries',wait_for_exit_fills:true,validity:'effective_session_only',historical_backfill:false,holdings_are_not_buy_instructions:true,rebalance_existing_positions:false,exit_scope:'strategy_and_position_entry_date',deduplicate_by:'action_id',changed_action_requires_review:true,account_mode:'subscriber_selected_paper_or_live',broker_orders_authorized_by_this_data:false}; const strategies = {free:'MI_FREE_WEEKLY_RISK_STATE_V1',pro:'MI_PRO_DYNAMIC_HALF_STAGGERED_V1'}; function validate(v, tier, hash) { keys(v,['schema','publisher','product_version','tier','strategy_id','issue_id','issue_date','completed_equity_session','timezone','kind','evidence','status','rules','actions']); requireThat(v.schema==='mi.newsletter.delta.v1' && v.publisher==='Mount Inflection' && v.product_version==='MI 2.1', 'Unrecognized newsletter version'); requireThat(v.tier===tier && v.strategy_id===strategies[tier], 'Wrong newsletter tier/strategy'); requireThat(v.issue_id==='mi21-'+tier+'-'+day(v.issue_date), 'Invalid issue identity'); requireThat(day(v.completed_equity_session)<=v.issue_date && v.timezone==='America/New_York', 'Invalid source date/timezone'); requireThat(v.kind==='entry_exit_delta' && v.evidence==='hypothetical_model_signals_not_fills', 'Invalid signal type'); requireThat(canonical(v.rules)===canonical(rules), 'Execution rules changed'); requireThat(['new_actions','no_new_actions','review_only'].includes(v.status) && Array.isArray(v.actions) && v.actions.length<=100, 'Invalid action status/count'); requireThat((v.status==='new_actions') === (v.actions.length>0), 'Action/status mismatch'); const ids=new Set(), groups={}; let openingSeen=false; v.actions.forEach(a=>{ keys(a,['action_id','symbol','action','position_side','signal_date','effective_session','position_entry_date','sizing']); requireThat(/^[A-Z][A-Z0-9.\-]{0,14}$/.test(a.symbol), 'Invalid symbol'); requireThat((tier==='free'?['BUY','SELL']:['BUY','SELL','SHORT','COVER']).includes(a.action), 'Unsupported action'); const enter=['BUY','SHORT'].includes(a.action), side=['BUY','SELL'].includes(a.action)?'long':'short'; requireThat(a.position_side===side, 'Side mismatch'); requireThat(day(a.signal_date)<=v.completed_equity_session && day(a.effective_session)>v.completed_equity_session && a.effective_session>=v.issue_date, 'Invalid signal/session dates'); requireThat(enter?day(a.position_entry_date)===a.effective_session:day(a.position_entry_date)<=a.signal_date, 'Invalid position episode'); requireThat(enter || !openingSeen, 'Exits must precede entries'); openingSeen=openingSeen||enter; const identity=[v.strategy_id,a.symbol,a.action,a.signal_date,a.effective_session,a.position_entry_date]; requireThat(a.action_id===hash(JSON.stringify(identity)) && !ids.has(a.action_id), 'Invalid or duplicate action identity'); ids.add(a.action_id); if(enter) { keys(a.sizing,['model_equity_fraction','subscriber_mapping_required']); requireThat(typeof a.sizing.model_equity_fraction==='number' && a.sizing.model_equity_fraction>0 && a.sizing.model_equity_fraction<=1 && a.sizing.subscriber_mapping_required===true,'Invalid sizing reference'); } else { requireThat(canonical(a.sizing)===canonical({close:'entire_strategy_owned_episode',if_not_owned:'skip'}),'Invalid exit ownership'); } (groups[a.symbol]||(groups[a.symbol]=[])).push(a.action); }); requireThat(new Set(v.actions.map(a=>a.effective_session)).size<=1,'Mixed effective sessions'); Object.values(groups).forEach(g=>requireThat(g.length===1 || (g.length===2 && ['SELL,SHORT','BUY,COVER'].includes(g.slice().sort().join(','))),'Conflicting symbol actions')); return v; } function discover(bodies,tier,hash) { requireThat(bodies.length>0 && bodies.length<=10,'Missing/ambiguous MIME bodies'); const values=[]; bodies.forEach(body=>{ requireThat(body.length<300000,'Message too large'); const begin='MI_AUTOMATION_JSON_BEGIN',end='MI_AUTOMATION_JSON_END'; const bc=body.split(begin).length-1,ec=body.split(end).length-1; if(!bc&&!ec)return; requireThat(bc===1&&ec===1&&body.indexOf(end)>body.indexOf(begin),'Conflicting or truncated footer'); const raw=body.split(begin)[1].split(end)[0].trim(),parsed=parseAt(raw,0); requireThat(raw.slice(parsed.end).trim()==='','Unexpected trailing JSON'); values.push(validate(parsed.value,tier,hash)); }); requireThat(values.length>0,'No v1 JSON footer: wait for a newly generated newsletter'); requireThat(values.every(v=>canonical(v)===canonical(values[0])),'HTML/plain-text footer conflict'); return values[0]; } function config(p) { const mode=p.ALPACA_MODE,tier=p.MI_TIER; requireThat(['paper','live'].includes(mode)&&['free','pro'].includes(tier),'Choose ALPACA_MODE paper/live and MI_TIER free/pro'); const capital=number(p.CAPITAL_PERCENT,'CAPITAL_PERCENT')/100,entry=number(p.MAX_ENTRY_PERCENT,'MAX_ENTRY_PERCENT')/100; const gross=number(p.MAX_GROSS_PERCENT,'MAX_GROSS_PERCENT')/100,maxOrder=number(p.MAX_ORDER_DOLLARS,'MAX_ORDER_DOLLARS'),cashBuffer=number(p.CASH_BUFFER_DOLLARS||'0','CASH_BUFFER_DOLLARS'); requireThat(capital>0&&capital<=1&&entry>0&&entry<=1&&gross>0&&gross<=2&&maxOrder>=1&&cashBuffer>=0,'Sizing limits out of range'); requireThat(tier!=='free'||(gross<=1&&p.ALLOW_SHORTS!=='true'),'Free is long-only without leverage'); requireThat(['true','false'].includes(p.ALLOW_SHORTS||'false'),'ALLOW_SHORTS must be true or false'); return {mode,tier,capital,entry,gross,maxOrder,cashBuffer,shorts:p.ALLOW_SHORTS==='true',account:p.EXPECTED_ACCOUNT_ID||''}; } function positionsMatch(positions,owned,active) { const expected={}; Object.keys(owned).forEach(s=>expected[s]=number(owned[s].qty,'ledger quantity')*(owned[s].side==='short'?-1:1)); if(active) active.rows.forEach(r=>{ if(r.applied||!r.order)return; const q=number(r.order.filled_qty||'0','partial fill'); expected[r.action.symbol]=(expected[r.action.symbol]||0)+(r.payload.side==='buy'?q:-q); }); const actual={}; positions.forEach(p=>{ requireThat(!(p.symbol in actual),'Duplicate broker position');actual[p.symbol]=number(p.qty,'position quantity'); }); for(const s of new Set(Object.keys(actual).concat(Object.keys(expected)))) requireThat(Math.abs((actual[s]||0)-(expected[s]||0))<0.00000001,'Untracked position or quantity drift: '+s); } function selections(v,owned,recent,hash) { const exits=[],entries=[],skipped=[]; v.actions.forEach(a=>{ const old=recent[a.action_id]; if(old) { requireThat(old.digest===hash(canonical(a)),'Action changed after processing');skipped.push({symbol:a.symbol,reason:'Already processed'});return; } if(['SELL','COVER'].includes(a.action)) { const held=owned[a.symbol]; if(!held){skipped.push({symbol:a.symbol,reason:'No strategy-owned position'});return;} requireThat(held.entry===a.position_entry_date&&held.side===a.position_side,'Exit episode mismatch: '+a.symbol); exits.push(a); } else { const held=owned[a.symbol],closing=exits.find(x=>x.symbol===a.symbol); if(held&&!closing) { requireThat(held.entry===a.position_entry_date&&held.side===a.position_side,'Unexpected existing episode: '+a.symbol); skipped.push({symbol:a.symbol,reason:'Already owned; no top-up'});return; } entries.push(a); } }); return {exits,entries,skipped}; } return {text,canonical,parseAt,day,number,validate,discover,config,positionsMatch,selections}; })(); if(typeof module!=='undefined')module.exports=MINewsletter; /* MI Newsletter Trader v1.0 beta — one private project and dedicated account per strategy. * Credentials stay in Script Properties. No trading occurs until you explicitly enable it. * Free paper predecessor was observed end to end; this generalized release is test-backed beta. */ const MI_VERSION='1.0.1-beta', MI_TZ='America/New_York'; function miProps_(){return PropertiesService.getScriptProperties();} function miAssert_(ok,message){if(!ok)throw Error(message);} function miHash_(s){return Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256,s,Utilities.Charset.UTF_8).map(b=>('0'+((b+256)%256).toString(16)).slice(-2)).join('');} function miToday_(){return Utilities.formatDate(new Date(),MI_TZ,'yyyy-MM-dd');} function miMinutes_(){const t=Utilities.formatDate(new Date(),MI_TZ,'HH:mm').split(':');return Number(t[0])*60+Number(t[1]);} function miConfig_(){return MINewsletter.config(miProps_().getProperties());} function miFingerprint_(c){const p=miProps_();return miHash_(MINewsletter.canonical(c)+':'+miHash_((p.getProperty('ALPACA_'+c.mode.toUpperCase()+'_KEY')||'')+':'+(p.getProperty('ALPACA_'+c.mode.toUpperCase()+'_SECRET')||'')));} function miReport_(v){const r=Object.assign({version:MI_VERSION,checkedAt:new Date().toISOString()},v),raw=JSON.stringify(r);miProps_().setProperty('MI_LAST_REPORT',raw.length<=6000?raw:JSON.stringify({version:MI_VERSION,checkedAt:r.checkedAt,status:r.status,detail:'Full report is in Executions; journal retains the latest receipt.'}));console.log(raw);return r;} function miLoad_(){ const p=miProps_(),head=p.getProperty('MI_STATE_HEAD'); if(!head)return {binding:null,owned:{},recent:{},issues:{},active:null,lastReceipt:null,lastIssueDate:null}; const h=JSON.parse(head);let raw='';for(let i=0;ik.startsWith('MI_S_')&&!k.startsWith('MI_S_'+generation+'_')).forEach(k=>p.deleteProperty(k)); } function miLock_(fn){const l=LockService.getScriptLock();miAssert_(l.tryLock(1000),'Another run is active; try again later');try{return fn();}finally{l.releaseLock();}} function miBinding_(c){return c.mode+':'+c.tier+':'+c.account;} function miCheckBinding_(s,c){miAssert_(!s.binding||s.binding===miBinding_(c),'This project is bound to another account/mode/tier. Use a separate project and account.');} function miCanTrade_(c){ miAssert_(miProps_().getProperty('MI_ARMED')==='true','Trading is paused'); miAssert_(miProps_().getProperty('MI_ARMED_CONFIG')===miFingerprint_(c),'Settings/credentials changed: preview and enable again'); if(c.mode==='live')miAssert_(miProps_().getProperty('LIVE_CONFIRMATION')==='I ACCEPT LIVE TRADING','Live confirmation missing'); } function miApi_(c,path,method,body,missing){ miAssert_(/^\/v2\//.test(path)&&!path.includes('://'),'Invalid broker path');method=method||'get'; miAssert_(method==='get'||(method==='post'&&path==='/v2/orders'),'Unsupported broker mutation'); if(method==='post')miCanTrade_(c); const p=miProps_(),label=c.mode.toUpperCase(),key=p.getProperty('ALPACA_'+label+'_KEY'),secret=p.getProperty('ALPACA_'+label+'_SECRET'); miAssert_(key&&secret&&key.startsWith(c.mode==='paper'?'PK':'AK'),'Missing or mismatched '+c.mode+' credentials'); const opt={method,muteHttpExceptions:true,followRedirects:false,headers:{'APCA-API-KEY-ID':key,'APCA-API-SECRET-KEY':secret}}; if(body){opt.contentType='application/json';opt.payload=JSON.stringify(body);} const root=c.mode==='paper'?'https://paper-api.alpaca.markets':'https://api.alpaca.markets'; const r=UrlFetchApp.fetch(root+path,opt),code=r.getResponseCode();if(missing&&code===404)return null; miAssert_(code>=200&&code<300,'Alpaca HTTP '+code+' on '+path.split('?')[0]+'. Review account/order status.'); return JSON.parse(r.getContentText()); } function miAccount_(c,connectionOnly){ const a=miApi_(c,'/v2/account'); miAssert_(connectionOnly||(/^[0-9a-f-]{36}$/.test(c.account)&&a.id===c.account),'EXPECTED_ACCOUNT_ID does not match'); miAssert_(a.status==='ACTIVE'&&!a.trading_blocked&&!a.account_blocked&&!a.trade_suspended_by_user,'Account not available for trading'); miAssert_(a.currency==='USD','Only USD accounts are supported'); if(c.shorts||c.gross>1)miAssert_(Number(a.multiplier)>1,'Shorts/leverage require a margin-enabled account'); return a; } function miOpenOrders_(c){const o=miApi_(c,'/v2/orders?status=open&limit=500');miAssert_(o.length<500,'Open order pagination requires review');return o;} function miQuote_(c,symbol){ const p=miProps_(),label=c.mode.toUpperCase(); const r=UrlFetchApp.fetch('https://data.alpaca.markets/v2/stocks/'+encodeURIComponent(symbol)+'/quotes/latest?feed=iex',{muteHttpExceptions:true,followRedirects:false,headers:{'APCA-API-KEY-ID':p.getProperty('ALPACA_'+label+'_KEY'),'APCA-API-SECRET-KEY':p.getProperty('ALPACA_'+label+'_SECRET')}}); miAssert_(r.getResponseCode()===200,'Quote unavailable: '+symbol);const q=JSON.parse(r.getContentText()).quote; miAssert_(q&&Number(q.ap)>0&&Number(q.bp)>0&&Date.now()-Date.parse(q.t)>=-60000&&Date.now()-Date.parse(q.t)<120000,'Fresh regular-session quote required: '+symbol); return q; } function miGmail_(path){ const r=UrlFetchApp.fetch('https://gmail.googleapis.com/gmail/v1/users/me/'+path,{muteHttpExceptions:true,followRedirects:false,headers:{Authorization:'Bearer '+ScriptApp.getOAuthToken()}}); miAssert_(r.getResponseCode()===200,'Gmail HTTP '+r.getResponseCode()+': check consent and Gmail API service');return JSON.parse(r.getContentText()); } function miHeader_(m,name){return (m.payload.headers||[]).filter(h=>h.name.toLowerCase()===name.toLowerCase()).map(h=>h.value);} function miAuthentic_(m){ const from=miHeader_(m,'From'),auth=miHeader_(m,'Authentication-Results').filter(s=>/^mx\.google\.com;/.test(s)); miAssert_(from.length===1&&/^(?:Mount Inflection|"Mount Inflection") $/i.test(from[0]),'Unexpected newsletter sender'); miAssert_(auth.length===1&&/dkim=pass\s+header\.i=@mountinflection\.com\b/.test(auth[0])&&/dmarc=pass[^;]*header\.from=mountinflection\.com\b/.test(auth[0]),'Gmail DKIM/DMARC evidence missing or ambiguous'); miAssert_(!(m.labelIds||[]).some(x=>['DRAFT','SPAM','TRASH'].includes(x)),'Draft/spam/trash source rejected'); } function miBodies_(message){ const bodies=[];function walk(p){if(p.filename)return;if(p.body&&p.body.data){const v=Utilities.newBlob(Utilities.base64DecodeWebSafe(p.body.data)).getDataAsString('UTF-8');if(p.mimeType==='text/plain')bodies.push(v);if(p.mimeType==='text/html')bodies.push(MINewsletter.text(v));}(p.parts||[]).forEach(walk);} walk(message.payload);return bodies; } function miReadIssue_(c,latest){ const subject='Mount Inflection '+(c.tier==='free'?'Free weekly view':'Pro daily view')+' — '; const q='from:hello@mountinflection.com newer_than:8d -in:trash -in:spam -in:drafts subject:"'+(c.tier==='free'?'Free weekly view':'Pro daily view')+'"'; const page=miGmail_('messages?q='+encodeURIComponent(q)+'&maxResults=30');miAssert_(!page.nextPageToken,'Too many newsletter matches; review mailbox'); const candidates=[]; (page.messages||[]).forEach(ref=>{const m=miGmail_('messages/'+ref.id+'?format=full'),h=miHeader_(m,'Subject');if(h.length!==1||!h[0].startsWith(subject))return;const d=h[0].slice(subject.length);if(!/^\d{4}-\d{2}-\d{2}$/.test(d)||d>miToday_()||(!latest&&d!==miToday_()))return;miAuthentic_(m);candidates.push({m,date:d});}); if(!candidates.length)return null; candidates.sort((a,b)=>b.date.localeCompare(a.date));const matches=candidates.filter(x=>x.date===candidates[0].date); miAssert_(matches.length===1,'Multiple/corrected matching issues: manual review required'); const found=matches[0],v=MINewsletter.discover(miBodies_(found.m),c.tier,miHash_); miAssert_(v.issue_date===found.date,'Subject/footer date conflict'); if(c.tier==='free')miAssert_(new Date(v.issue_date+'T12:00:00Z').getUTCDay()===5,'Free issue must be Friday'); return {value:v,digest:miHash_(MINewsletter.canonical(v)),messageId:found.m.id}; } function miInitial_(c,s){ miCheckBinding_(s,c);const a=miAccount_(c),positions=miApi_(c,'/v2/positions'); miAssert_(!s.active,'An unfinished run needs reconciliation first');miAssert_(miOpenOrders_(c).length===0,'Open orders must be reviewed first'); MINewsletter.positionsMatch(positions,s.owned,null);return {account:a,positions}; } function checkConnections(){return miLock_(()=>{const c=miConfig_(),a=miAccount_(c,true);miGmail_('messages?maxResults=1&q='+encodeURIComponent('from:hello@mountinflection.com'));return miReport_({status:'connections_ok_no_orders',mode:c.mode,tier:c.tier,accountId:a.id,accountSuffix:a.account_number.slice(-4),next:'Copy accountId into EXPECTED_ACCOUNT_ID, then run previewLatestIssue.'});});} function previewLatestIssue(){return miLock_(()=>{ const c=miConfig_(),s=miLoad_(),p=miInitial_(c,s),source=miReadIssue_(c,true);miAssert_(source,'No delivered issue found: confirm your Free/Pro subscription and wait for its next email'); const selected=MINewsletter.selections(source.value,s.owned,s.recent,miHash_); selected.entries.forEach(a=>miAsset_(c,a)); selected.entries.forEach(a=>{const amount=Math.floor(Number(p.account.equity)*c.capital*a.sizing.model_equity_fraction*100)/100;miAssert_(amount>=1&&amount<=c.maxOrder&&c.capital*a.sizing.model_equity_fraction<=c.entry,'Preview exceeds entry limit or is below $1: '+a.symbol);}); const equity=Number(p.account.equity),preview={status:'preview_no_orders',mode:c.mode,tier:c.tier,accountSuffix:p.account.account_number.slice(-4),issue:source.value.issue_id,sourceSession:source.value.completed_equity_session,equityBasis:equity,capitalPercent:c.capital*100,exits:selected.exits.map(a=>({symbol:a.symbol,action:a.action,qty:s.owned[a.symbol].qty})),entries:selected.entries.map(a=>({symbol:a.symbol,action:a.action,referenceDollars:Math.floor(equity*c.capital*a.sizing.model_equity_fraction*100)/100})),skipped:selected.skipped,limits:{maxEntryPercent:c.entry*100,maxOrderDollars:c.maxOrder,maxGrossPercent:c.gross*100},note:'Preview only. Execution rechecks dates, positions, cash, quotes, borrow availability and limits. Shorts round down to whole shares.'}; miProps_().setProperty('MI_PREVIEW',JSON.stringify({fingerprint:miFingerprint_(c),at:Date.now(),issue:source.value.issue_id}));return miReport_(preview); });} function miRemoveTriggers_(handlers){ScriptApp.getProjectTriggers().filter(t=>handlers.includes(t.getHandlerFunction())).forEach(t=>ScriptApp.deleteTrigger(t));} function installSchedule(){return miLock_(()=>{ const c=miConfig_(),s=miLoad_();miInitial_(c,s);miProps_().setProperty('MI_ARMED','false');miRemoveTriggers_(['runScheduled','continueRun']); let b=ScriptApp.newTrigger('runScheduled').timeBased();b=c.tier==='free'?b.everyWeeks(1).onWeekDay(ScriptApp.WeekDay.FRIDAY):b.everyDays(1); b.atHour(9).nearMinute(45).inTimezone(MI_TZ).create(); if(!s.binding){s.binding=miBinding_(c);s.started=miToday_();miSave_(s);} return miReport_({status:'schedule_installed_trading_paused',tier:c.tier,mode:c.mode,schedule:c.tier==='free'?'Friday around 09:45 Eastern':'Daily around 09:45 Eastern',timing:'Google allows approximately ±15 minutes; no exact opening-price promise',next:'Run previewLatestIssue, review the account and sizing, then explicitly enable your selected mode.'}); });} function miEnable_(mode){return miLock_(()=>{ const c=miConfig_(),s=miLoad_();miAssert_(c.mode===mode,'Wrong enable function for selected mode');miInitial_(c,s); const proof=JSON.parse(miProps_().getProperty('MI_PREVIEW')||'null');miAssert_(proof&&proof.fingerprint===miFingerprint_(c)&&Date.now()-proof.at<7*86400000,'Run previewLatestIssue with these settings first'); miAssert_(ScriptApp.getProjectTriggers().some(t=>t.getHandlerFunction()==='runScheduled'),'Run installSchedule first'); if(mode==='live')miAssert_(miProps_().getProperty('LIVE_CONFIRMATION')==='I ACCEPT LIVE TRADING','Set LIVE_CONFIRMATION explicitly before enabling real-money orders'); miAssert_(s.binding===miBinding_(c),'Schedule/account binding missing'); miProps_().setProperties({MI_ARMED_CONFIG:miFingerprint_(c),MI_ARMED:'true'},false); return miReport_({status:'trading_enabled',mode,tier:c.tier,note:'No order submitted by enable. The next scheduled run can trade current eligible newsletter actions.'}); });} function enablePaperTrading(){return miEnable_('paper');} function enableLiveTrading(){return miEnable_('live');} function pauseTrading(){ // Stop submissions even while another invocation holds the journal lock. miProps_().setProperty('MI_ARMED','false');miRemoveTriggers_(['runScheduled','continueRun']); return miReport_({status:'paused',note:'Future submissions stopped. An in-flight request or accepted order may still fill; inspect orders and holdings in Alpaca.'}); } function miResumeAt_(at){miRemoveTriggers_(['continueRun']);const when=Math.max(Date.now()+60000,at||Date.now()+180000);ScriptApp.newTrigger('continueRun').timeBased().at(new Date(when)).create();} function miAsset_(c,a){ const asset=miApi_(c,'/v2/assets/'+encodeURIComponent(a.symbol));miAssert_(asset.status==='active'&&asset.tradable&&asset.class==='us_equity'&&asset.exchange!=='OTC','Unsupported equity: '+a.symbol); if(a.action==='BUY')miAssert_(asset.fractionable,'Fractional long entries unavailable: '+a.symbol); if(a.action==='SHORT')miAssert_(c.tier==='pro'&&c.shorts&&asset.shortable&&asset.easy_to_borrow,'Short permission/borrow unavailable: '+a.symbol); return asset; } function miPrepareEntries_(c,active,a,positions){ const equity=Number(a.equity);miAssert_(equity>0,'Account equity must be positive'); let gross=positions.reduce((n,p)=>n+Math.abs(Number(p.market_value)),0),long=positions.reduce((n,p)=>n+Math.max(0,Number(p.market_value)),0),cash=Number(a.cash)-c.cashBuffer,bp=Number(a.buying_power); const rows=[]; for(const action of active.selection.entries){ miAsset_(c,action);const fraction=action.sizing.model_equity_fraction; const dollars=Math.floor(equity*c.capital*fraction*100)/100; miAssert_(fraction*c.capital<=c.entry&&dollars>=1&&dollars<=c.maxOrder,'Entry exceeds your sizing limit or is below $1: '+action.symbol); let payload={symbol:action.symbol,side:action.action==='BUY'?'buy':'sell',type:'market',time_in_force:'day'}; if(action.action==='SHORT'){ // Alpaca reserves opening-short buying power using 3% above ask. IEX quote is a sizing reference, not a guaranteed fill. const q=miQuote_(c,action.symbol),price=Number(q.ap)*1.03,qty=Math.floor(dollars/price); miAssert_(qty>=1,'Short allocation is smaller than one share: '+action.symbol);payload.qty=String(qty);bp-=qty*price;gross+=qty*price; }else{payload.notional=dollars.toFixed(2);cash-=dollars;bp-=dollars;gross+=dollars;long+=dollars;} miAssert_(bp>=0&&cash>=0&&gross<=equity*c.gross,'Insufficient cash/buying power or gross exposure limit'); // This version does not borrow cash for long entries, even when Pro shorts are enabled. miAssert_(long<=equity-c.cashBuffer,'Long holdings exceed cash-only long allocation limit'); rows.push({action,payload,attempted:false,applied:false,order:null}); } active.equityBasis=equity;return rows; } function miViewOrder_(o){return {id:o.id,client_order_id:o.client_order_id,symbol:o.symbol,side:o.side,status:o.status,filled_qty:o.filled_qty,filled_avg_price:o.filled_avg_price};} function miApplyOrder_(s,r,o){ miAssert_(o.client_order_id===r.payload.client_order_id&&o.symbol===r.payload.symbol&&o.side===r.payload.side&&o.type==='market'&&o.time_in_force==='day','Broker order does not match journal'); if(r.payload.qty)miAssert_(Math.abs(Number(o.qty)-Number(r.payload.qty))<1e-8,'Broker quantity mismatch'); if(r.payload.notional)miAssert_(Number(o.notional)===Number(r.payload.notional),'Broker notional mismatch'); r.order=miViewOrder_(o); miAssert_(!['rejected','canceled','expired','suspended','replaced'].includes(o.status),'Order '+o.status+': '+o.symbol+'; inspect partial fills before recovery'); if(o.status==='filled'&&!r.applied){ miAssert_(Number(o.filled_qty)>0,'Filled order has no quantity'); if(['BUY','SHORT'].includes(r.action.action))s.owned[r.action.symbol]={side:r.action.position_side,qty:o.filled_qty,entry:r.action.position_entry_date,orderId:o.id}; else delete s.owned[r.action.symbol]; r.applied=true;s.recent[r.action.action_id]={digest:miHash_(MINewsletter.canonical(r.action)),date:r.action.effective_session}; } } function miReconcile_(c,s){ for(const r of s.active.rows){ if(!r.payload.client_order_id)continue; const o=miApi_(c,'/v2/orders:by_client_order_id?client_order_id='+encodeURIComponent(r.payload.client_order_id),'get',null,true); if(o)miApplyOrder_(s,r,o);else miAssert_(!r.attempted,'Order submission is uncertain: do not resend; verify with Alpaca'); } miSave_(s);MINewsletter.positionsMatch(miApi_(c,'/v2/positions'),s.owned,s.active); const allowed=new Set(s.active.rows.map(r=>r.payload.client_order_id).filter(Boolean)); miAssert_(miOpenOrders_(c).every(o=>allowed.has(o.client_order_id)),'Unrelated open order in dedicated account'); } function miRun_(){return miLock_(()=>{ if(miProps_().getProperty('MI_ARMED')!=='true')return miReport_({status:'paused_no_orders'}); try{ const c=miConfig_();miCanTrade_(c);const s=miLoad_();miCheckBinding_(s,c);miAccount_(c); const today=miToday_(); if(!s.active){ if(c.tier==='free'&&new Date(today+'T12:00:00Z').getUTCDay()!==5)return miReport_({status:'outside_free_schedule'}); miInitial_(c,s);const source=miReadIssue_(c,false); if(!source){miAssert_(miMinutes_()<645,'No current newsletter by 10:45 Eastern; review before trading');miResumeAt_();return miReport_({status:'waiting_for_current_newsletter'});} const v=source.value;miAssert_(!s.lastIssueDate||v.issue_date>=s.lastIssueDate,'Older issue rejected'); if(s.issues[v.issue_id]){miAssert_(s.issues[v.issue_id].digest===source.digest,'Issue changed after processing');return miReport_({status:'duplicate_issue_skipped',issue:v.issue_id});} miAssert_(v.issue_date>=s.started,'No historical backfill'); const selection=MINewsletter.selections(v,s.owned,s.recent,miHash_); selection.entries.forEach(a=>miAsset_(c,a)); s.active={source,selection,phase:'exits',rows:selection.exits.map(a=>({action:a,payload:{symbol:a.symbol,side:a.action==='SELL'?'sell':'buy',qty:s.owned[a.symbol].qty,type:'market',time_in_force:'day'},attempted:false,applied:false,order:null})),observations:0,createdAt:new Date().toISOString(),effective:v.actions.length?v.actions[0].effective_session:today}; miSave_(s); } const active=s.active;active.observations++;miAssert_(active.observations<=80,'Reconciliation budget exhausted; review required'); miReconcile_(c,s); const pending=active.selection.exits.length+active.selection.entries.length>0; if(pending){ miAssert_(today<=active.effective,'Signal expired; no next-day replay'); const clock=miApi_(c,'/v2/clock'); if(todayr.applied)){ const positions=miApi_(c,'/v2/positions'); active.entryRows=miPrepareEntries_(c,active,miAccount_(c),positions);active.rows=active.rows.concat(active.entryRows);delete active.entryRows;active.phase='entries';miSave_(s); } // Only start new submissions in the morning. Existing orders can still be reconciled later. const unsent=active.rows.filter(r=>!r.applied&&!r.attempted); miAssert_(!unsent.length||(miMinutes_()>=570&&miMinutes_()<=645),'New order outside 09:30–10:45 Eastern window'); let submissions=0; for(const r of active.rows){ if(r.applied||r.attempted)continue; if(submissions>=8)break; // Stay below the Apps Script execution limit; continue with the same journal. miCanTrade_(c);miAssert_(miApi_(c,'/v2/clock').is_open,'Market closed before submission'); if(['BUY','SHORT'].includes(r.action.action)){ miAsset_(c,r.action);const a=miAccount_(c),cost=r.payload.notional?Number(r.payload.notional):Number(r.payload.qty)*Number(miQuote_(c,r.action.symbol).ap)*1.03; miAssert_(cost<=c.maxOrder&&Number(a.buying_power)>=cost,'Order exceeds current buying power/size cap'); if(r.payload.notional)miAssert_(Number(a.cash)-c.cashBuffer>=cost,'Insufficient current cash'); } r.payload.client_order_id='mi-nl-v1-'+miHash_(miBinding_(c)+':'+r.action.action_id).slice(0,32);miSave_(s); const existing=miApi_(c,'/v2/orders:by_client_order_id?client_order_id='+encodeURIComponent(r.payload.client_order_id),'get',null,true); if(existing){r.attempted=true;miApplyOrder_(s,r,existing);miSave_(s);continue;} r.attempted=true;miSave_(s); // A timeout after this point NEVER causes an automatic resubmission. const o=miApi_(c,'/v2/orders','post',r.payload);miApplyOrder_(s,r,o);miSave_(s);submissions++; } if(active.phase==='exits'||!active.rows.every(r=>r.applied)){miResumeAt_();return miReport_({status:'reconciling_orders',mode:c.mode,issue:active.source.value.issue_id,phase:active.phase,filled:active.rows.filter(r=>r.applied).length,total:active.rows.length});} const v=active.source.value,receipt={status:'rotation_complete',mode:c.mode,tier:c.tier,issue:v.issue_id,completedAt:new Date().toISOString(),filled:active.rows.length,skipped:active.selection.skipped,orders:active.rows.map(r=>r.order)}; s.issues[v.issue_id]={digest:active.source.digest,date:v.issue_date};s.lastIssueDate=v.issue_date;s.lastReceipt=receipt;s.active=null; const cutoff=new Date(Date.now()-8*86400000).toISOString().slice(0,10);for(const k of Object.keys(s.recent))if(s.recent[k].date{ const c=miConfig_(),s=miLoad_();miCheckBinding_(s,c);miAccount_(c);miAssert_(s.active,'No unfinished run; use previewLatestIssue and the enable function'); miAssert_(miProps_().getProperty('MI_ARMED_CONFIG')===miFingerprint_(c),'Recovery cannot change execution settings'); miAssert_(miToday_()===s.active.effective,'Expired run needs manual account reconciliation');miReconcile_(c,s); if(c.mode==='live')miAssert_(miProps_().getProperty('LIVE_CONFIRMATION')==='I ACCEPT LIVE TRADING','Live confirmation missing'); miProps_().setProperty('MI_ARMED','true');miResumeAt_();return miReport_({status:'reconciled_resume_scheduled',note:'Future invocation may submit remaining orders. No order submitted by this function.'}); });} function showStatus(){return miLock_(()=>{const c=miConfig_(),s=miLoad_();miCheckBinding_(s,c);const a=miAccount_(c);return miReport_({status:'account_status_no_orders',mode:c.mode,tier:c.tier,armed:miProps_().getProperty('MI_ARMED')==='true',accountSuffix:a.account_number.slice(-4),cash:a.cash,equity:a.equity,positions:miApi_(c,'/v2/positions').map(p=>({symbol:p.symbol,qty:p.qty})),openOrders:miOpenOrders_(c).map(miViewOrder_),active:s.active?{issue:s.active.source.value.issue_id,phase:s.active.phase}:null,lastReceipt:s.lastReceipt,triggers:ScriptApp.getProjectTriggers().map(t=>t.getHandlerFunction())});});}