Initial commit

This commit is contained in:
kdusek
2025-12-09 12:13:01 +01:00
commit 8e654ed209
13332 changed files with 2695056 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
from edgar.ownership.html_render import ownership_to_html
from edgar.ownership.ownershipforms import (
Address,
DerivativeHolding,
DerivativeHoldings,
DerivativeTransaction,
DerivativeTransactions,
Footnotes,
Form3,
Form4,
Form5,
Issuer,
NonDerivativeHolding,
NonDerivativeHoldings,
NonDerivativeTransaction,
NonDerivativeTransactions,
Owner,
Ownership,
OwnerSignature,
PostTransactionAmounts,
ReportingOwners,
ReportingRelationship,
TransactionCode,
TransactionSummary,
translate_ownership,
)

View File

@@ -0,0 +1,198 @@
import re
from decimal import Decimal
from typing import Any, Optional, Tuple, Union
import numpy as np
import pandas as pd
from lxml import etree
__all__ = ['is_numeric', 'compute_average_price', 'compute_total_value',
'format_currency', 'format_amount', 'safe_numeric', 'format_numeric']
def is_numeric(series: pd.Series) -> bool:
if np.issubdtype(series.dtype, np.number):
return True
try:
series.astype(float)
return True
except ValueError:
return False
def compute_average_price(shares: pd.Series, price: pd.Series) -> Decimal:
"""
Compute the average price of the trades
:param shares: The number of shares as a series
:param price: The price per share as a series
:return:
"""
if is_numeric(shares) and is_numeric(price):
shares = pd.to_numeric(shares)
price = pd.to_numeric(price)
value = (shares * price).sum() / shares.sum()
return Decimal(str(value)).quantize(Decimal('0.01'))
def compute_total_value(shares: pd.Series, price: pd.Series) -> Decimal:
"""
Compute the total value of the trades
:param shares: The number of shares as a series
:param price: The price per share as a series
:return:
"""
if is_numeric(shares) and is_numeric(price):
shares = pd.to_numeric(shares)
price = pd.to_numeric(price)
value = (shares * price).sum()
return Decimal(str(value)).quantize(Decimal('0.01'))
def format_currency(amount: Union[int, float]) -> str:
if amount is None or np.isnan(amount):
return ""
if isinstance(amount, (int, float)):
return f"${amount:,.2f}"
return str(amount)
def format_amount(amount: Union[int, float]) -> str:
if amount is None:
return ""
if isinstance(amount, (int, float)):
# Can it be formatted as an integer?
if amount == int(amount):
return f"{amount:,.0f}"
return f"{amount:,.2f}"
return str(amount)
def safe_numeric(value: Any) -> Optional[Union[int, float]]:
"""
Safely convert a value to a number, handling footnote references and other special cases.
Args:
value: The value to convert (could be string, int, float, or None)
Returns:
Numeric value if conversion is possible, None otherwise
"""
if value is None or pd.isna(value):
return None
# If already a number, return as is
if isinstance(value, (int, float)) and not np.isnan(value):
return value
# Handle string cases
if isinstance(value, str):
# Remove commas, dollar signs, and whitespace
cleaned = value.replace(',', '').replace('$', '').strip()
# Remove footnote references like [F1], [1], etc.
cleaned = re.sub(r'\[\w+]', '', cleaned)
# Try numeric conversion
try:
# Check if it's an integer
if '.' not in cleaned:
return int(cleaned)
else:
return float(cleaned)
except (ValueError, TypeError):
return None
return None
def format_numeric(value: Any, currency: bool = False, default: str = "N/A") -> str:
"""
Format a potentially non-numeric value for display, handling special cases.
Args:
value: The value to format
currency: Whether to format as currency with $ symbol
default: Default string to return if value can't be converted to number
Returns:
Formatted string representation
"""
number = safe_numeric(value)
if number is None:
# If the original value was a string with content, return it instead of default
if isinstance(value, str) and value.strip():
return value.strip()
return default
if currency:
return f"${number:,.2f}"
else:
# Format integers without decimal places
if isinstance(number, int) or (isinstance(number, float) and number.is_integer()):
return f"{int(number):,}"
return f"{number:,.2f}"
# Add a dedicated currency formatter
def format_price(value: Any, default: str = "N/A") -> str:
"""Format a price value with currency symbol"""
return format_numeric(value, currency=True, default=default)
def _get_xml_value(elem: etree._Element, xpath: str, with_footnote: bool = True) -> str:
"""Helper to safely get text from XML element with value tag and optional footnote
Args:
elem: XML element to search within
xpath: XPath expression to find target element
with_footnote: Whether to include footnote references in output
Returns:
Text value with optional footnote reference
"""
# Handle both direct text elements and those with <value> wrapper
nodes = elem.xpath(xpath)
if not nodes:
return ''
node = nodes[0]
# Try to get text from <value> child first
value = node.find('value')
if value is not None:
text = value.text if value.text else ''
else:
# If no <value> tag, get direct text content
text = node.text if node.text else ''
# Add footnote reference if present and requested
if with_footnote:
footnote = node.find('footnoteId')
if footnote is not None:
footnote_id = footnote.get('id', '')
if footnote_id:
text = f"{text}<sup>({footnote_id})</sup>"
return text.strip()
def _parse_name(name: str) -> Tuple[str, str, str]:
"""Parse a full name into (last, first, middle) components"""
# Remove any extra whitespace
name = ' '.join(name.split())
parts = name.split(' ')
if len(parts) == 1:
return (parts[0], '', '')
elif len(parts) == 2:
return (parts[1], parts[0], '')
else:
return (parts[-1], parts[0], ' '.join(parts[1:-1]))
def _format_xml_date(date_str: str) -> str:
"""Format YYYY-MM-DD to MM/DD/YYYY"""
if not date_str:
return ''
try:
year, month, day = date_str.split('-')
return f"{month}/{day}/{year}"
except ValueError:
return date_str

View File

@@ -0,0 +1,390 @@
# html_render.py - HTML rendering module for ownership forms
import os
import re
from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING:
from edgar.ownership.core import Ownership
import pandas as pd
from jinja2 import Environment, FileSystemLoader
from edgar.ownership.core import format_numeric, format_price
def _format_date(date_str: str) -> str:
if not date_str or pd.isna(date_str):
return "N/A"
try:
return pd.to_datetime(date_str).strftime('%m/%d/%Y')
except (ValueError, TypeError):
return str(date_str) # Return original if parsing fails
def _escape_html(value: Any) -> str:
"""Escape HTML special characters in a string."""
if value is None or (isinstance(value, float) and pd.isna(value)):
return "N/A"
s_val = str(value)
# Only escape HTML special characters (Jinja2 will handle this automatically in templates)
# but we still need it for our cell content preparation
return s_val.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def format_owner_name(owner_name: Optional[str]) -> str:
"""Format owner name for display."""
if not owner_name:
return ""
return _escape_html(owner_name)
def format_address(street1: Optional[str], street2: Optional[str], city: Optional[str], state: Optional[str], zip_code: Optional[str]) -> str:
"""Format address components into a HTML address string."""
parts = []
if street1:
parts.append(_escape_html(street1))
if street2:
parts.append(_escape_html(street2))
city_state_zip_line_parts = []
if city:
city_state_zip_line_parts.append(_escape_html(city))
if state:
city_state_zip_line_parts.append(_escape_html(state))
if zip_code:
city_state_zip_line_parts.append(_escape_html(zip_code))
if city_state_zip_line_parts:
parts.append(" ".join(city_state_zip_line_parts).strip())
return "<br>".join(part for part in parts if part)
def ownership_to_html(ownership: 'Ownership') -> str:
"""Convert an Ownership object to HTML format matching official SEC layout.
Args:
ownership: Ownership object containing SEC form data
Returns:
HTML string representation
"""
# Set up Jinja2 environment
template_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
env = Environment(loader=FileSystemLoader(template_dir))
template = env.get_template('ownership_form.html')
# Extract basic information and prepare context
form_type = ownership.form
# Prepare context dictionary for template rendering
context = {'form_type': form_type, 'html_title': f"SEC Form {form_type}", 'form_name_display': f"FORM {form_type}",
'form_title_display': {
'3': "INITIAL STATEMENT OF BENEFICIAL OWNERSHIP OF SECURITIES",
'4': "STATEMENT OF CHANGES IN BENEFICIAL OWNERSHIP",
'5': "ANNUAL STATEMENT OF CHANGES IN BENEFICIAL OWNERSHIP"
}.get(form_type, "STATEMENT OF OWNERSHIP"),
'issuer_name': _escape_html(ownership.issuer.name if ownership.issuer else "N/A"),
'ticker': _escape_html(ownership.issuer.ticker if ownership.issuer else "N/A"),
'reporting_period': _format_date(ownership.reporting_period) if ownership.reporting_period else ''}
# Issuer information
# Reporting owner information
reporting_owner = ownership.reporting_owners.owners[0] if ownership.reporting_owners.owners else None
if reporting_owner:
context['reporting_owner_name_str'] = format_owner_name(reporting_owner.name)
context['reporting_owner_address_str'] = format_address(
reporting_owner.address.street1 if reporting_owner.address else None,
reporting_owner.address.street2 if reporting_owner.address else None,
reporting_owner.address.city if reporting_owner.address else None,
reporting_owner.address.state_or_country if reporting_owner.address else None,
reporting_owner.address.zipcode if reporting_owner.address else None
)
# Reporting owner relationship
context['is_director'] = 'X' if reporting_owner.is_director else ''
context['is_officer'] = 'X' if reporting_owner.is_officer else ''
context['is_ten_pct'] = 'X' if reporting_owner.is_ten_pct_owner else ''
context['is_other'] = 'X' if reporting_owner.is_other else ''
context['officer_title'] = _escape_html(reporting_owner.officer_title) if reporting_owner.officer_title else ''
# Remarks
context['remarks'] = _escape_html(ownership.remarks) if ownership.remarks else ''
# Footnotes
if ownership.footnotes and ownership.footnotes._footnotes:
footnotes_list = []
for footnote in ownership.footnotes._footnotes:
footnotes_list.append(f"<p>{_escape_html(footnote)}</p>")
context['footnotes_html'] = "\n".join(footnotes_list)
else:
context['footnotes_html'] = ''
# Signature
if ownership.signatures and ownership.signatures.signatures:
first_signature = ownership.signatures.signatures[0]
if first_signature.signature:
context['sig_name'] = _escape_html(first_signature.signature)
if first_signature.date:
if isinstance(first_signature.date, str):
context['sig_date'] = _escape_html(_format_date(first_signature.date))
else:
context['sig_date'] = _escape_html(str(first_signature.date))
# Process Table I - Non-Derivative Securities
non_deriv_rows = []
non_derivative_table = getattr(ownership, 'non_derivative_table', None)
if non_derivative_table:
# Form 3 - Initial holdings
if form_type == '3' and hasattr(non_derivative_table, 'holdings') and non_derivative_table.holdings is not None and not non_derivative_table.holdings.empty:
for holding_tuple in non_derivative_table.holdings.data.itertuples(index=False):
# Convert tuple to dictionary for easier access with default values
holding = {col: getattr(holding_tuple, col, '') for col in holding_tuple._fields}
# Extract values using dictionary get() with defaults
security_title = str(holding.get('Security', ''))
shares_owned = format_numeric(str(holding.get('Shares', '')))
direct_indirect_code = str(holding.get('DirectIndirect', ''))
nature_of_ownership = str(holding.get('NatureOfOwnership', ''))
# Clean footnotes from values for table display
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
row = [
f'<td class="TableCell">{security_title_clean}</td>',
f'<td class="TableCell Right">{shares_owned}</td>',
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
f'<td class="TableCell">{nature_of_ownership}</td>'
]
non_deriv_rows.append(row)
# Form 4/5 - Transactions
elif form_type in ['4', '5'] and hasattr(non_derivative_table, 'transactions') and non_derivative_table.transactions is not None and not non_derivative_table.transactions.empty:
for transaction_tuple in non_derivative_table.transactions.data.itertuples(index=False):
# Convert tuple to dictionary for easier access with default values
transaction = {col: getattr(transaction_tuple, col, '') for col in transaction_tuple._fields}
# Extract values using dictionary get() with defaults
security_title = str(transaction.get('Security', ''))
transaction_date = _format_date(transaction.get('Date', ''))
deemed_date = _format_date(transaction.get('DeemedDate', ''))
transaction_code = str(transaction.get('Code', ''))
transaction_v = str(transaction.get('V', ''))
shares = format_numeric(str(transaction.get('Shares', '')))
acquired_disposed = str(transaction.get('AcquiredDisposed', ''))
price = format_price(transaction.get('Price', ''))
owned_after_transaction = format_numeric(str(transaction.get('Remaining', '')))
direct_indirect_code = str(transaction.get('DirectIndirect', ''))
nature_of_ownership = str(transaction.get('NatureOfOwnership', ''))
# Clean footnotes from values for table display
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
row = [
f'<td class="TableCell">{security_title_clean}</td>',
f'<td class="TableCell">{transaction_date}</td>',
f'<td class="TableCell">{deemed_date}</td>',
f'<td class="TableCell">{transaction_code}</td>',
f'<td class="TableCell">{transaction_v}</td>',
f'<td class="TableCell Right">{shares}</td>',
f'<td class="TableCell Centered">{acquired_disposed}</td>',
f'<td class="TableCell Right">{price}</td>',
f'<td class="TableCell Right">{owned_after_transaction}</td>',
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
f'<td class="TableCell">{nature_of_ownership}</td>'
]
non_deriv_rows.append(row)
# Process Holdings for Forms 4/5
if hasattr(non_derivative_table, 'holdings') and non_derivative_table.holdings is not None and not non_derivative_table.holdings.empty:
for holding_tuple in non_derivative_table.holdings.data.itertuples(index=False):
# Convert tuple to dictionary for easier access with default values
holding = {col: getattr(holding_tuple, col, '') for col in holding_tuple._fields}
# Extract values using dictionary get() with defaults
security_title = str(holding.get('Security', ''))
shares_owned = format_numeric(str(holding.get('Shares', '')))
direct_indirect_code = str(holding.get('DirectIndirect', ''))
nature_of_ownership = str(holding.get('NatureOfOwnership', ''))
# Clean footnotes from values for table display
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
row = [
f'<td class="TableCell">{security_title_clean}</td>',
'<td class="TableCell"></td>', # Transaction Date
'<td class="TableCell"></td>', # Deemed Execution Date
'<td class="TableCell"></td>', # Transaction Code
'<td class="TableCell"></td>', # V
'<td class="TableCell"></td>', # Amount
'<td class="TableCell"></td>', # A/D
'<td class="TableCell"></td>', # Price
f'<td class="TableCell Right">{shares_owned}</td>',
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
f'<td class="TableCell">{nature_of_ownership}</td>'
]
non_deriv_rows.append(row)
# Add non_deriv_rows to context
context['non_deriv_rows'] = non_deriv_rows
# Process Table II - Derivative Securities
deriv_rows = []
derivative_table = getattr(ownership, 'derivative_table', None)
if derivative_table:
# Form 3 - Initial derivative holdings
if form_type == '3' and hasattr(derivative_table, 'holdings') and derivative_table.holdings is not None and not derivative_table.holdings.empty:
for holding_tuple in derivative_table.holdings.data.itertuples(index=False):
# Convert tuple to dictionary for easier access with default values
holding = {col: getattr(holding_tuple, col, '') for col in holding_tuple._fields}
# Extract values using dictionary get() with defaults
security_title = str(holding.get('Security', ''))
conversion_price = format_price(holding.get('ExercisePrice', ''))
exercisable_date = _format_date(holding.get('ExerciseDate', ''))
expiration_date = _format_date(holding.get('ExpirationDate', ''))
exercisable_expiration = f"{exercisable_date} - {expiration_date}"
underlying_title = str(holding.get('Underlying', ''))
underlying_shares = format_numeric(holding.get('UnderlyingShares', ''))
title_amount_underlying = f"{underlying_title} - {underlying_shares}"
direct_indirect_code = holding.get('DirectIndirect', '')
nature_of_ownership = str(holding.get('Nature Of Ownership', ''))
# Clean footnotes from values for table display
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
exercisable_expiration_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', exercisable_expiration).strip()
title_amount_underlying_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', title_amount_underlying).strip()
row = [
f'<td class="TableCell">{security_title_clean}</td>',
f'<td class="TableCell Right">{conversion_price}</td>',
'<td class="TableCell">N/A</td>', # Transaction Date (blank for F3 initial holding)
'<td class="TableCell">N/A</td>', # Deemed Execution Date (blank for F3 initial holding)
'<td class="TableCell">N/A</td>', # Transaction Code (blank for F3 initial holding)
'<td class="TableCell">N/A</td>', # V (blank for F3 initial holding)
'<td class="TableCell">N/A</td>', # Shares in transaction (blank for F3 initial holding)
'<td class="TableCell">N/A</td>', # A/D (blank for F3 initial holding)
f'<td class="TableCell">{exercisable_expiration_clean}</td>',
f'<td class="TableCell">{title_amount_underlying_clean}</td>',
'<td class="TableCell">N/A</td>', # Price (blank for F3 initial holding)
'<td class="TableCell">N/A</td>', # Amount owned after transaction (blank for F3 initial holding)
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
f'<td class="TableCell">{nature_of_ownership}</td>'
]
deriv_rows.append(row)
# Form 4/5 - Derivative Transactions
elif form_type in ['4', '5'] and hasattr(derivative_table, 'transactions') and derivative_table.transactions is not None and not derivative_table.transactions.empty:
for transaction_tuple in derivative_table.transactions.data.itertuples(index=False):
# Convert tuple to dictionary for easier access with default values
transaction = {col: getattr(transaction_tuple, col, '') for col in transaction_tuple._fields}
# Extract values using dictionary get() with defaults
security_title = str(transaction.get('Security', ''))
conversion_price = format_price(transaction.get('ExercisePrice', ''))
transaction_date = _format_date(transaction.get('Date', ''))
deemed_date = _format_date(transaction.get('DeemedDate', ''))
transaction_code = str(transaction.get('Code', ''))
transaction_v = str(transaction.get('V', ''))
shares = format_numeric(str(transaction.get('Shares', '')))
acquired_disposed = str(transaction.get('AcquiredDisposed', ''))
exercisable_date = _format_date(transaction.get('ExerciseDate', ''))
expiration_date = _format_date(transaction.get('ExpirationDate', ''))
exercisable_expiration = f"{exercisable_date} - {expiration_date}"
underlying_title = str(transaction.get('Underlying', ''))
underlying_shares = format_numeric(transaction.get('UnderlyingShares', ''))
title_amount_underlying = f"{underlying_title} - {underlying_shares}"
price = format_price(transaction.get('Price', ''))
owned_after_transaction = format_numeric(str(transaction.get('Remaining', '')))
direct_indirect_code = str(transaction.get('DirectIndirect', ''))
nature_of_ownership = str(transaction.get('NatureOfOwnership', ''))
# Clean footnotes from values for table display
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
exercisable_expiration_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', exercisable_expiration).strip()
title_amount_underlying_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', title_amount_underlying).strip()
row = [
f'<td class="TableCell">{security_title_clean}</td>',
f'<td class="TableCell Right">{conversion_price}</td>',
f'<td class="TableCell">{transaction_date}</td>',
f'<td class="TableCell">{deemed_date}</td>',
f'<td class="TableCell">{transaction_code}</td>',
f'<td class="TableCell">{transaction_v}</td>',
f'<td class="TableCell Right">{shares}</td>',
f'<td class="TableCell Centered">{acquired_disposed}</td>',
f'<td class="TableCell">{exercisable_expiration_clean}</td>',
f'<td class="TableCell">{title_amount_underlying_clean}</td>',
f'<td class="TableCell Right">{price}</td>',
f'<td class="TableCell Right">{owned_after_transaction}</td>',
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
f'<td class="TableCell">{nature_of_ownership}</td>'
]
deriv_rows.append(row)
# Process Holdings for Forms 4/5
if hasattr(derivative_table, 'holdings') and derivative_table.holdings:
for holding_tuple in derivative_table.holdings.data.itertuples(index=False):
# Convert tuple to dictionary for easier access with default values
holding = {col: getattr(holding_tuple, col, '') for col in holding_tuple._fields}
# Extract values using dictionary get() with defaults
security_title = str(holding.get('Security', ''))
conversion_price = format_price(holding.get('ExercisePrice', ''))
exercisable_date = _format_date(holding.get('ExerciseDate', ''))
expiration_date = _format_date(holding.get('ExpirationDate', ''))
exercisable_expiration = f"{exercisable_date} - {expiration_date}"
underlying_title = str(holding.get('Underlying', ''))
underlying_shares = format_numeric(holding.get('UnderlyingShares', ''))
title_amount_underlying = f"{underlying_title} - {underlying_shares}"
owned_after_transaction = format_numeric(holding.get('Remaining', ''))
direct_indirect_code = holding.get('DirectIndirect', '')
nature_of_ownership = str(holding.get('NatureOfOwnership', ''))
# Clean footnotes from values for table display
security_title_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', security_title).strip()
exercisable_expiration_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', exercisable_expiration).strip()
title_amount_underlying_clean = re.sub(r'<[Ss][Uu][Pp]>.*?</[Ss][Uu][Pp]>', '', title_amount_underlying).strip()
row = [
f'<td class="TableCell">{security_title_clean}</td>',
f'<td class="TableCell Right">{conversion_price}</td>',
'<td class="TableCell"></td>', # Transaction Date
'<td class="TableCell"></td>', # Deemed Execution Date
'<td class="TableCell"></td>', # Transaction Code
'<td class="TableCell"></td>', # V
'<td class="TableCell"></td>', # Shares in transaction
'<td class="TableCell"></td>', # A/D
f'<td class="TableCell">{exercisable_expiration_clean}</td>',
f'<td class="TableCell">{title_amount_underlying_clean}</td>',
'<td class="TableCell"></td>', # Price
f'<td class="TableCell Right">{owned_after_transaction}</td>',
f'<td class="TableCell Centered">{direct_indirect_code}</td>',
f'<td class="TableCell">{nature_of_ownership}</td>'
]
deriv_rows.append(row)
# Add deriv_rows to context
context['deriv_rows'] = deriv_rows
# Render the template with the context
return template.render(**context)
def _parse_name(name: str):
"""Parse a full name into (last, first, middle) components"""
if not name:
return '', '', ''
# Check for comma format: "Last, First Middle"
if ',' in name:
last, rest = name.split(',', 1)
parts = rest.strip().split()
first = parts[0] if parts else ''
middle = ' '.join(parts[1:]) if len(parts) > 1 else ''
else:
# Assume format "First Middle Last"
parts = name.split()
last = parts[-1] if parts else ''
first = parts[0] if parts else ''
middle = ' '.join(parts[1:-1]) if len(parts) > 1 else ''
return last.strip(), first.strip(), middle.strip()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
<!doctype html public "-//w3c//dtd xhtml">
<html>
<head>
<title>{{ html_title }}</title>
<style>
body {font-family: Arial, sans-serif; margin: 20px; font-size: 9pt; color: #000;}
table {border-collapse: collapse; width: 100%;}
th, td {border: 1px solid #777; padding: 3px; text-align: left; vertical-align: top; font-size:8pt;}
th {background-color: #f0f0f0; font-weight: bold; text-align:center;}
.TableCell {font-size:8pt;}
.Centered {text-align: center;}
.Right {text-align: right;}
.Bold {font-weight: bold;}
.SmallText {font-size: 7pt;}
.FormTitle {font-size: 12pt; font-weight: bold; text-align: center; margin-bottom: 5px;}
.Header {font-size: 10pt; font-weight: bold; text-align: center; margin-bottom: 5px;}
.SubHeader {font-size: 9pt; text-align: center; margin-bottom: 15px;}
.SectionHeader {font-size: 10pt; font-weight: bold; margin-top: 10px; margin-bottom: 5px;}
.IssuerInfo, .ReportingOwnerInfo {border: 1px solid #000; padding: 5px; margin-bottom:10px; width: 48%; display:inline-block; vertical-align:top;}
.InfoBoxTable td {border:none; font-size:8pt; padding:1px;}
.Footnotes {margin-top: 20px; font-size: 8pt;}
.FootnoteList {list-style-type: none; padding-left: 0;}
.FootnoteList li {margin-bottom: 3px;}
.SignatureBlock {margin-top:30px; font-size:9pt;}
.SignatureBlock .SignatureDate {float:right;}
.Remarks {margin-top:15px; font-size:9pt;}
.RemarkText {margin-left:10px; font-size:8pt;}
.Checkbox {display: inline-block; width: 12px; height: 12px; border: 1px solid #000; text-align: center; line-height: 12px;}
.NoBorder, .NoBorder td, .NoBorder th {border: none;}
</style>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,234 @@
{% extends "base.html" %}
{% block content %}
<!-- OMB Header -->
<div class="SmallText Right">
OMB APPROVAL<br>
OMB Number: 3235-0287<br>
Estimated average burden<br>
hours per response: 0.5
</div>
<div class="FormTitle">{{ form_name_display }}</div>
<div class="SubHeader">Check this box if no longer subject to Section 16. Form 4 or Form 5 obligations may continue. See Instruction 1(b). <span class="Checkbox"></span></div>
<div class="Header">UNITED STATES SECURITIES AND EXCHANGE COMMISSION<br>Washington, D.C. 20549</div>
<div class="Header">{{ form_title_display }}</div>
<div class="SmallText Centered" style="margin-bottom:10px;">Filed pursuant to Section 16(a) of the Securities Exchange Act of 1934<br>or Section 30(h) of the Investment Company Act of 1940</div>
<table class="NoBorder" style="margin-bottom:10px;">
<tr>
<td style="width:60%; vertical-align:top;">
<div><span class="Bold">1. Name and Address of Reporting Person*</span></div>
<div style="margin-left:10px; margin-top:2px;">
{{ reporting_owner_name_str }}<br>
{{ reporting_owner_address_str|safe }}
</div>
<div style="margin-top:5px;"><span class="Bold">1a. IRS/SSN Identification Number of Reporting Person</span></div>
<div style="margin-left:10px; margin-top:2px;">&nbsp;</div>
</td>
<td style="width:40%; vertical-align:top;">
<div><span class="Bold">2. Issuer Name and Ticker or Trading Symbol</span></div>
<div style="margin-left:10px; margin-top:2px;">{{ issuer_name }} [{{ ticker }}]</div>
<div style="margin-top:5px;"><span class="Bold">3. Date of Earliest Transaction (Month/Day/Year)</span></div>
<div style="margin-left:10px; margin-top:2px;">{{ reporting_period }}</div>
<div style="margin-top:5px;"><span class="Bold">4. If Amendment, Date of Original Filed (Month/Day/Year)</span></div>
<div style="margin-left:10px; margin-top:2px;">&nbsp;</div>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top:5px;">
<span class="Bold">5. Relationship of Reporting Person(s) to Issuer</span><br>
<span class="SmallText">(Check all applicable)</span>
<div style="margin-left:10px; margin-top:2px;">
<span class="Checkbox">{% if is_director %}X{% else %}&nbsp;{% endif %}</span> Director
&nbsp;&nbsp; <span class="Checkbox">{% if is_officer %}X{% else %}&nbsp;{% endif %}</span> Officer (give title below)
&nbsp;&nbsp; <span class="Checkbox">{% if is_ten_pct_owner %}X{% else %}&nbsp;{% endif %}</span> 10% Owner <br>
<span class="Checkbox">{% if is_other %}X{% else %}&nbsp;{% endif %}</span> Other (specify below)
<div style="margin-left:25px; margin-top:2px; font-size:7pt;">{{ officer_title }}</div>
<div style="margin-left:25px; margin-top:2px; font-size:7pt;">{{ relationship_str }}</div>
</div>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top:5px;">
<span class="Bold">6. Individual or Joint/Group Filing (Check Applicable Line)</span>
<div style="margin-left:10px; margin-top:2px;">
<span class="Checkbox">{% if is_individual_filing %}X{% else %}&nbsp;{% endif %}</span> Form filed by One Reporting Person<br>
<span class="Checkbox">{% if is_joint_filing %}X{% else %}&nbsp;{% endif %}</span> Form filed by More than One Reporting Person
</div>
</td>
</tr>
</table>
<!-- Table I - Non-Derivative Securities -->
<h4 class="SectionHeader" style="text-align:center;">
Table I - Non-Derivative Securities {% if form_type == '3' %}Beneficially Owned{% else %}Acquired, Disposed of, or Beneficially Owned{% endif %}
</h4>
<table class="ReportTable">
<thead>
{% if form_type == '3' %}
<!-- Form 3 Table I Header -->
<tr>
<th>1. Title of Security (Instr. 3)</th>
<th>2. Amount of Securities Beneficially Owned (Instr. 4)</th>
<th>3. Ownership Form: Direct (D) or Indirect (I) (Instr. 4)</th>
<th>4. Nature of Indirect Beneficial Ownership (Instr. 4)</th>
</tr>
{% else %}
<!-- Forms 4 & 5 Table I Header -->
<tr>
<th rowspan="2">1. Title of Security (Instr. 3)</th>
<th rowspan="2">2. Transaction Date (Month/Day/Year)</th>
<th rowspan="2">2A. Deemed Execution Date, if any (Month/Day/Year)</th>
<th colspan="2">3. Transaction Code (Instr. 8)</th>
<th colspan="3">4. Securities Acquired (A) or Disposed Of (D) (Instr. 3, 4 and 5)</th>
<th rowspan="2">5. Amount of Securities Beneficially<br/>Owned Following Reported Transaction(s)<br/>(Instr. 3 and 4)</th>
<th rowspan="2">6. Ownership Form: Direct (D) or Indirect (I) <br/>(Instr. 4)</th>
<th rowspan="2">7. Nature of Indirect Beneficial Ownership <br/>(Instr. 4)</th>
</tr>
<tr>
<th>Code</th>
<th>V</th>
<th>Amount</th>
<th>(A) or (D)</th>
<th>Price</th>
</tr>
{% endif %}
</thead>
<tbody>
{% if non_deriv_rows %}
{% for row in non_deriv_rows %}
<tr>
{% for cell in row %}
{{ cell|safe }}
{% endfor %}
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="{% if form_type == '3' %}4{% else %}11{% endif %}" class="Centered">No non-derivative securities reported</td>
</tr>
{% endif %}
</tbody>
</table>
<!-- Table II - Derivative Securities -->
<h4 class="SectionHeader" style="text-align:center; margin-top:20px;">
{% if form_type == '3' %}
Table II - Derivative Securities Beneficially Owned
{% else %}
Table II - Derivative Securities Acquired, Disposed of, or Beneficially Owned
{% endif %}
<br>
<span style="font-weight:normal; font-size:8pt;">(e.g., puts, calls, warrants, options, convertible securities)</span>
</h4>
<table class="ReportTable">
<thead>
{% if form_type == '3' %}
<!-- Form 3 Table II Header -->
<tr>
<th>1. Title of Derivative Security (Instr. 5)</th>
<th>2. Date Exercisable and Expiration Date (Month/Day/Year)</th>
<th>3. Title and Amount of Securities Underlying Derivative Security (Instr. 4)</th>
<th>4. Conversion or Exercise Price of Derivative Security</th>
<th>5. Ownership Form: Direct (D) or Indirect (I) (Instr. 5)</th>
<th>6. Nature of Indirect Beneficial Ownership (Instr. 5)</th>
</tr>
{% else %}
<!-- Forms 4 & 5 Table II Header -->
<tr>
<th rowspan="2">1. Title of Derivative Security (Instr. 5)</th>
<th rowspan="2">2. Conversion or Exercise Price of Derivative Security</th>
<th rowspan="2">3. Transaction Date (Month/Day/Year)</th>
<th rowspan="2">3A. Deemed Execution Date, if any (Month/Day/Year)</th>
<th colspan="2">4. Transaction Code (Instr. 8)</th>
<th colspan="2">5. Number of Derivative Securities Acquired (A) or Disposed of (D) (Instr. 3, 4 and 5)</th>
<th rowspan="2">6. Date Exercisable and Expiration Date (Month/Day/Year)</th>
<th rowspan="2">7. Title and Amount of Securities Underlying Derivative Security (Instr. 3 and 4)</th>
<th rowspan="2">8. Price of Derivative Security</th>
<th rowspan="2">9. Number of derivative Securities Beneficially Owned Following Reported Transaction(s) (Instr. 4)</th>
<th rowspan="2">10. Ownership Form of Derivative Security: Direct (D) or Indirect (I) (Instr. 4)</th>
<th rowspan="2">11. Nature of Indirect Beneficial Ownership (Instr. 5)</th>
</tr>
<tr>
<th>Code</th>
<th>V</th>
<th>Amount or Number of Shares</th>
<th>(A) or (D)</th>
</tr>
{% endif %}
</thead>
<tbody>
{% if deriv_rows %}
{% for row in deriv_rows %}
<tr>
{% for cell in row %}
{{ cell|safe }}
{% endfor %}
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="{% if form_type == '3' %}6{% else %}16{% endif %}" class="Centered">No derivative securities reported</td>
</tr>
{% endif %}
</tbody>
</table>
<!-- Remarks Section -->
{% if remarks %}
<div class="Remarks">
<span class="Bold">Remarks:</span><br>
<div class="RemarkText">{{ remarks }}</div>
</div>
{% endif %}
<!-- Footnotes Section -->
{% if footnotes %}
<div class="Footnotes">
<h4 class="SectionHeader">Explanation of Responses:</h4>
<div class="FootnoteList">
{% for footnote in footnotes %}
<li><span class="Bold">{{ footnote.id }}:</span> {{ footnote.text }}</li>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Signature Section -->
<div class="SignatureBlock">
<table style="width:auto; border:none;">
<tr>
{% if sig_name %}
<td style="padding-right:30px;">
<div class="SmallText">** Signature of Reporting Person</div>
<div>/s/ {{ sig_name }}</div>
</td>
{% else %}
<td></td>
{% endif %}
{% if sig_date %}
<td>
<div class="SmallText">Date</div>
<div>{{ sig_date }}</div>
</td>
{% else %}
<td></td>
{% endif %}
</tr>
</table>
</div>
<!-- Disclaimer and OMB Notice -->
<div style="margin-top:10px; font-size:7pt; font-style:italic;">
Note: File three copies of this Form, one of which must be manually signed. If space is insufficient, see Instruction 6 for procedure.<br>
Persons who respond to the collection of information contained in this form are not required to respond unless the form displays a currently valid OMB Number.
</div>
{% endblock %}